diff options
Diffstat (limited to 'libhw_cr')
-rw-r--r-- | libhw_cr/CMakeLists.txt | 43 | ||||
-rw-r--r-- | libhw_cr/alarmclock.c | 23 | ||||
-rw-r--r-- | libhw_cr/host_alarmclock.c | 133 | ||||
-rw-r--r-- | libhw_cr/host_include/libhw/host_alarmclock.h | 27 | ||||
-rw-r--r-- | libhw_cr/host_include/libhw/host_net.h | 44 | ||||
-rw-r--r-- | libhw_cr/host_net.c | 534 | ||||
-rw-r--r-- | libhw_cr/host_util.c | 21 | ||||
-rw-r--r-- | libhw_cr/host_util.h | 47 | ||||
-rw-r--r-- | libhw_cr/rp2040_dma.c | 56 | ||||
-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 | 244 | ||||
-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 | 962 | ||||
-rw-r--r-- | libhw_cr/w5500_ll.h | 426 |
19 files changed, 3206 insertions, 0 deletions
diff --git a/libhw_cr/CMakeLists.txt b/libhw_cr/CMakeLists.txt new file mode 100644 index 0000000..caeac21 --- /dev/null +++ b/libhw_cr/CMakeLists.txt @@ -0,0 +1,43 @@ +# 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 SYSTEM 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 + ) + 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 SYSTEM 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..2f255e0 --- /dev/null +++ b/libhw_cr/host_alarmclock.c @@ -0,0 +1,133 @@ +/* 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 <error.h> +#include <signal.h> + +#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 = {0}, + }; + 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 = {0}, + }; + 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..89df68a --- /dev/null +++ b/libhw_cr/host_include/libhw/host_alarmclock.h @@ -0,0 +1,27 @@ +/* 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 <stdbool.h> /* for bool */ +#include <time.h> /* for clockid_t, timer_t */ + +#include <libmisc/private.h> +#include <libhw/generic/alarmclock.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..fced229 --- /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..f1c988c --- /dev/null +++ b/libhw_cr/host_net.c @@ -0,0 +1,534 @@ +/* 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 */ +#include <error.h> /* for error(3gnu) */ +#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/assert.h> +#include <libmisc/macro.h> +#include <libobj/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 = {0}; + + 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 = {0}; \ + 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 bool RUN_PTHREAD(void *(*fn)(void *), void *args) { + pthread_t thread; + bool saved = cr_save_and_disable_interrupts(); + if (pthread_create(&thread, NULL, fn, args)) + return true; + cr_pause_and_yield(); + cr_restore_interrupts(saved); + if (pthread_join(thread, NULL)) + return true; + return false; +} + +enum hostnet_timeout_op { + OP_NONE, + OP_SEND, + OP_RECV, +}; + +static inline ssize_t hostnet_map_negerrno(ssize_t v, enum hostnet_timeout_op op) { + if (v >= 0) + return v; + switch (v) { + case -EHOSTUNREACH: + return -NET_EARP_TIMEOUT; + case -ETIMEDOUT: + switch (op) { + case OP_NONE: + assert_notreached("impossible ETIMEDOUT"); + case OP_SEND: + return -NET_EACK_TIMEOUT; + case OP_RECV: + return -NET_ERECV_TIMEOUT; + } + assert_notreached("invalid timeout op"); + case -EBADF: + return -NET_ECLOSED; + case -EMSGSIZE: + return -NET_EMSGSIZE; + default: + return -NET_EOTHER; + } +} + +/* 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 = { 0 }; + + 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; +}; + +static void *hostnet_pthread_accept(void *_args) { + struct hostnet_pthread_accept_args *args = _args; + *(args->ret_connfd) = accept(args->listenerfd, NULL, NULL); + if (*(args->ret_connfd) < 0) + *(args->ret_connfd) = -errno; + WAKE_COROUTINE(args); + return NULL; +} + +static lo_interface net_stream_conn hostnet_tcplist_accept(struct hostnet_tcp_listener *listener) { + assert(listener); + + int ret_connfd; + struct hostnet_pthread_accept_args args = { + .cr_thread = pthread_self(), + .cr_coroutine = cr_getcid(), + .listenerfd = listener->fd, + .ret_connfd = &ret_connfd, + }; + if (RUN_PTHREAD(hostnet_pthread_accept, &args)) + return LO_NULL(net_stream_conn); + + if (ret_connfd < 0) + return LO_NULL(net_stream_conn); + + listener->active_conn.fd = ret_connfd; + listener->active_conn.read_deadline_ns = 0; + return lo_box_hostnet_tcp_as_net_stream_conn(&listener->active_conn); +} + +/* TCP listener close() *******************************************************/ + +static int hostnet_tcplist_close(struct hostnet_tcp_listener *listener) { + assert(listener); + + return hostnet_map_negerrno(shutdown(listener->fd, SHUT_RDWR) ? -errno : 0, OP_NONE); +} + +/* 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; + + ssize_t *ret; +}; + +static void *hostnet_pthread_readv(void *_args) { + struct hostnet_pthread_readv_args *args = _args; + + *(args->ret) = setsockopt(args->connfd, SOL_SOCKET, SO_RCVTIMEO, + &args->timeout, sizeof(args->timeout)); + if (*(args->ret) < 0) + goto end; + + *(args->ret) = readv(args->connfd, args->iov, args->iovcnt); + if (*(args->ret) < 0) + goto end; + + end: + if (*(args->ret) < 0) + *(args->ret) = hostnet_map_negerrno(-errno, OP_SEND); + WAKE_COROUTINE(args); + return NULL; +} + +static ssize_t hostnet_tcp_readv(struct _hostnet_tcp_conn *conn, const struct iovec *iov, int iovcnt) { + assert(conn); + assert(iov); + assert(iovcnt > 0); + + ssize_t ret; + struct hostnet_pthread_readv_args args = { + .cr_thread = pthread_self(), + .cr_coroutine = cr_getcid(), + + .connfd = conn->fd, + .iov = iov, + .iovcnt = iovcnt, + + .ret = &ret, + }; + if (conn->read_deadline_ns) { + uint64_t now_ns = LO_CALL(bootclock, get_time_ns); + if (conn->read_deadline_ns < now_ns) + return -NET_ERECV_TIMEOUT; + args.timeout = ns_to_host_us_time(conn->read_deadline_ns-now_ns); + } else { + args.timeout = (host_us_time_t){0}; + } + + if (RUN_PTHREAD(hostnet_pthread_readv, &args)) + return -NET_ETHREAD; + return ret; +} + +/* TCP write() ****************************************************************/ + +struct hostnet_pthread_writev_args { + pthread_t cr_thread; + cid_t cr_coroutine; + + int connfd; + const struct iovec *iov; + int iovcnt; + + ssize_t *ret; +}; + +static void *hostnet_pthread_writev(void *_args) { + struct hostnet_pthread_writev_args *args = _args; + + size_t count = 0; + struct iovec *iov = alloca(sizeof(struct iovec)*args->iovcnt); + 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) { + hostnet_map_negerrno(-errno, OP_RECV); + 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) = done; + WAKE_COROUTINE(args); + return NULL; +} + +static ssize_t hostnet_tcp_writev(struct _hostnet_tcp_conn *conn, const struct iovec *iov, int iovcnt) { + assert(conn); + assert(iov); + assert(iovcnt > 0); + + ssize_t ret; + struct hostnet_pthread_writev_args args = { + .cr_thread = pthread_self(), + .cr_coroutine = cr_getcid(), + + .connfd = conn->fd, + .iov = iov, + .iovcnt = iovcnt, + + .ret = &ret, + }; + if (RUN_PTHREAD(hostnet_pthread_writev, &args)) + return -NET_ETHREAD; + return ret; +} + +/* TCP close() ****************************************************************/ + +static int hostnet_tcp_close(struct _hostnet_tcp_conn *conn) { + assert(conn); + return hostnet_map_negerrno(shutdown(conn->fd, SHUT_RDWR) ? -errno : 0, OP_NONE); +} +static int hostnet_tcp_close_read(struct _hostnet_tcp_conn *conn) { + assert(conn); + return hostnet_map_negerrno(shutdown(conn->fd, SHUT_RD) ? -errno : 0, OP_NONE); +} +static int hostnet_tcp_close_write(struct _hostnet_tcp_conn *conn) { + assert(conn); + return hostnet_map_negerrno(shutdown(conn->fd, SHUT_WR) ? -errno : 0, OP_NONE); +} + +/* 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 = { 0 }; + + 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 (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; + + ssize_t *ret; +}; + +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 = { 0 }; + + addr.in.sin_family = AF_INET; + addr.in.sin_addr.s_addr = + (((uint32_t)args->node.octets[0])<<24) | + (((uint32_t)args->node.octets[1])<<16) | + (((uint32_t)args->node.octets[2])<< 8) | + (((uint32_t)args->node.octets[3])<< 0) ; + addr.in.sin_port = htons(args->port); + *(args->ret) = sendto(args->connfd, args->buf, args->count, 0, &addr.gen, sizeof(addr)); + if (*(args->ret) < 0) + *(args->ret) = hostnet_map_negerrno(-errno, OP_SEND); + WAKE_COROUTINE(args); + return NULL; +} + +static ssize_t hostnet_udp_sendto(struct hostnet_udp_conn *conn, void *buf, size_t count, + struct net_ip4_addr node, uint16_t port) { + assert(conn); + + ssize_t ret; + 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 = &ret, + }; + if (RUN_PTHREAD(hostnet_pthread_sendto, &args)) + return -NET_ETHREAD; + return ret; +} + +/* 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; + + ssize_t *ret_size; + 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 = { 0 }; + socklen_t addr_size; + + *(args->ret_size) = setsockopt(args->connfd, SOL_SOCKET, SO_RCVTIMEO, + &args->timeout, sizeof(args->timeout)); + if (*(args->ret_size) < 0) + goto end; + + *(args->ret_size) = recvfrom(args->connfd, args->buf, args->count, + MSG_TRUNC, &addr.gen, &addr_size); + if (*(args->ret_size) < 0) + goto end; + + assert(addr.in.sin_family == AF_INET); + if (args->ret_node) { + args->ret_node->octets[0] = (addr.in.sin_addr.s_addr >> 24) & 0xFF; + args->ret_node->octets[1] = (addr.in.sin_addr.s_addr >> 16) & 0xFF; + args->ret_node->octets[2] = (addr.in.sin_addr.s_addr >> 8) & 0xFF; + args->ret_node->octets[3] = (addr.in.sin_addr.s_addr >> 0) & 0xFF; + } + if (args->ret_port) { + (*args->ret_port) = ntohs(addr.in.sin_port); + } + + end: + if (*(args->ret_size) < 0) + *(args->ret_size) = hostnet_map_negerrno(-errno, OP_RECV); + WAKE_COROUTINE(args); + return NULL; +} + +static ssize_t 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); + + ssize_t ret; + struct hostnet_pthread_recvfrom_args args = { + .cr_thread = pthread_self(), + .cr_coroutine = cr_getcid(), + + .connfd = conn->fd, + .buf = buf, + .count = count, + + .ret_size = &ret, + .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 -NET_ERECV_TIMEOUT; + args.timeout = ns_to_host_us_time(conn->read_deadline_ns-now_ns); + } else { + args.timeout = (host_us_time_t){0}; + } + + if (RUN_PTHREAD(hostnet_pthread_recvfrom, &args)) + return -NET_ETHREAD; + return ret; +} + +/* UDP close() ****************************************************************/ + +static int hostnet_udp_close(struct hostnet_udp_conn *conn) { + assert(conn); + + return hostnet_map_negerrno(close(conn->fd) ? -errno : 0, OP_NONE); +} diff --git a/libhw_cr/host_util.c b/libhw_cr/host_util.c new file mode 100644 index 0000000..958ed9c --- /dev/null +++ b/libhw_cr/host_util.c @@ -0,0 +1,21 @@ +/* libhw_cr/host_util.c - Utilities for GNU/Linux hosts + * + * Copyright (C) 2024 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include <error.h> /* for error(3gnu) */ +#include <signal.h> /* for SIGRTMIN, SIGRTMAX */ + +#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; +} diff --git a/libhw_cr/host_util.h b/libhw_cr/host_util.h new file mode 100644 index 0000000..8c53fab --- /dev/null +++ b/libhw_cr/host_util.h @@ -0,0 +1,47 @@ +/* libhw_cr/host_util.h - Utilities for GNU/Linux hosts + * + * Copyright (C) 2024 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 <time.h> /* for struct timespec */ +#include <sys/time.h> /* for struct timeval */ + +#include <libhw/generic/alarmclock.h> /* for {X}S_PER_S */ + +int host_sigrt_alloc(void); + +typedef struct timeval host_us_time_t; +typedef struct timespec host_ns_time_t; + +static inline 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; +} + +static inline 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; +} + +static inline 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)); +} + +static inline 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); +} + +#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..69116bf --- /dev/null +++ b/libhw_cr/rp2040_dma.c @@ -0,0 +1,56 @@ +/* libhw_cr/rp2040_dma.c - Utilities for sharing the DMA IRQs + * + * Copyright (C) 2025 Luke T. Shumaker <lukeshu@lukeshu.com> + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +#include <stdbool.h> + +#include <hardware/irq.h> /* for irq_set_exclusive_handler() */ + +#include "rp2040_dma.h" + +struct dmairq_handler_entry { + dmairq_handler_t fn; + void *arg; +}; +struct dmairq_handler_entry dmairq_handlers[NUM_DMA_CHANNELS] = {0}; + +bool dmairq_initialized[NUM_DMA_IRQS] = {0}; + +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..e295adf --- /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 <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> *********************************************/ + +static inline 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 ***************************************************************/ + +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_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..1ae74f9 --- /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/structs/io_bank0.h> /* for io_bank0_hw */ +#include <hardware/irq.h> /* for irq_set_exclusive_handler() */ + +#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] = {0}; + +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..d4adb11 --- /dev/null +++ b/libhw_cr/rp2040_hwspi.c @@ -0,0 +1,244 @@ +/* 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 <alloca.h> +#include <inttypes.h> /* for PRIu{n} */ + +#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/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); + debugf("clk_peri = %"PRIu32"Hz", clk_peri_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); + debugf("baudrate = %uHz", actual_baudrate_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){0}; + + /* Initialize the interrupt handler. */ + dmairq_set_and_enable_exclusive_handler(DMAIRQ_0, self->dma_rx_data, rp2040_hwspi_intrhandler, self); +} + +static void 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; + + int pruned_iovcnt = 0; + for (int i = 0; i < iovcnt; i++) + if (iov[i].iov_len) + pruned_iovcnt++; + if (!pruned_iovcnt) + return; + + /* For tx_data_blocks, 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). + * + * But for rx_data_blocks, we need ctrl to be the trigger + * register so that the DMA_CTRL_IRQ_QUIET flag isn't cleared + * before we get to the trigger; and while for tx_data_blocks + * it doesn't really matter, the inverse would be nice. + */ + struct dma_alias1 *tx_data_blocks = alloca(sizeof(struct dma_alias1)*(pruned_iovcnt+1)); + struct dma_alias0 *rx_data_blocks = alloca(sizeof(struct dma_alias0)*(pruned_iovcnt+1)); + + 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])){0}; + rx_data_blocks[pruned_iovcnt] = (typeof(rx_data_blocks[0])){0}; + + /* 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; +} diff --git a/libhw_cr/rp2040_hwtimer.c b/libhw_cr/rp2040_hwtimer.c new file mode 100644 index 0000000..8227abb --- /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_rp2040_hwtimer_as_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..9d99f7b --- /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..51effba --- /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..295add2 --- /dev/null +++ b/libhw_cr/w5500.c @@ -0,0 +1,962 @@ +/* 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 + * + * 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 <inttypes.h> /* for PRIu{n} */ + +/* TODO: Write a <libhw/generic/gpio.h> to avoid w5500.c being + * pico-sdk-specific. */ +#include <hardware/gpio.h> /* pico-sdk:hardware_gpio */ +#include "rp2040_gpioirq.h" + +#include <libcr/coroutine.h> /* for cr_yield() */ + +#include <libhw/generic/alarmclock.h> /* for sleep_*() */ + +#define LOG_NAME W5500 +#include <libmisc/log.h> /* for errorf(), debugf(), const_byte_str() */ + +#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); + } +} + +/* libobj *********************************************************************/ + +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); + n_debugf(W5500_LL, "w5500_irq_cr(): chipintr=%"PRIu8, 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); + n_debugf(W5500_LL, "w5500_irq_cr(): sockintr[%"PRIu8"]=%"PRIu8, 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) { + debugf("w5500_irq_cr(): signal sock[%"PRIu8"]->listen_sema", socknum); + cr_sema_signal(&socket->listen_sema); + } + if (recv_bits) { + debugf("w5500_irq_cr(): signal sock[%"PRIu8"]->read_sema", socknum); + cr_sema_signal(&socket->read_sema); + } + if (send_bits) { + debugf("w5500_irq_cr(): signal sock[%"PRIu8"]->write_ch", socknum); + _w5500_sockintr_ch_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)) { + debugf("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; + debugf("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) { + errorf("SPI to W5500 does not appear to be functional: wrote:0x%02"PRIx16" != read:0x%02"PRIx8, a, 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) { + debugf("if_up()"); + debugf(":: addr = "PRI_net_ip4_addr, ARG_net_ip4_addr(cfg.addr)); + debugf(":: gateway_addr = "PRI_net_ip4_addr, ARG_net_ip4_addr(cfg.gateway_addr)); + debugf(":: subnet_mask = "PRI_net_ip4_addr, ARG_net_ip4_addr(cfg.subnet_mask)); + _w5500_if_up(chip, cfg); +} + +static void w5500_if_ifdown(struct w5500 *chip) { + debugf("if_down()"); + _w5500_if_up(chip, (struct net_iface_config){0}); +} + +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) { + debugf("tcp_listen() => no sock"); + return LO_NULL(net_stream_listener); + } + debugf("tcp_listen() => sock[%"PRIu8"]", 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_w5500_tcplist_as_net_stream_listener(sock); +} + +static lo_interface net_stream_conn 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) { + debugf("tcp_dial() => no sock"); + return LO_NULL(net_stream_conn); + } + uint8_t socknum = socket->socknum; + debugf("tcp_dial() => sock[%"PRIu8"]", 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); + debugf("tcp_dial(): state=%s", w5500_state_str(state)); + switch (state) { + case STATE_TCP_SYNSENT: + cr_yield(); + break; + case STATE_TCP_ESTABLISHED: + return lo_box_w5500_tcp_as_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) { + debugf("udp_conn() => no sock"); + return LO_NULL(net_packet_conn); + } + uint8_t socknum = socket->socknum; + debugf("udp_conn() => sock[%"PRIu8"]", 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_w5500_udp_as_net_packet_conn(socket); +} + +/* tcp_listener methods *******************************************************/ + +static lo_interface net_stream_conn w5500_tcplist_accept(struct _w5500_socket *socket) { + ASSERT_SELF(stream_listener, TCP); + + restart: + if (!socket->list_open) { + debugf("tcp_listener.accept() => already closed"); + return LO_NULL(net_stream_conn); + } + + 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); + debugf("tcp_listener.accept() => state=%s", 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; + /* fall-through */ + case STATE_TCP_CLOSE_WAIT: + socket->write_open = true; + return lo_box_w5500_tcp_as_net_stream_conn(socket); + default: + goto restart; + } + } +} + +static int w5500_tcplist_close(struct _w5500_socket *socket) { + debugf("tcp_listener.close()"); + ASSERT_SELF(stream_listener, TCP); + + socket->list_open = false; + w5500_tcp_maybe_free(chip, socket); + return 0; +} + +/* tcp_conn methods ***********************************************************/ + +static ssize_t 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; + debugf("tcp_conn.write(%zu)", count); + ASSERT_SELF(stream_conn, TCP); + if (count == 0) + return 0; + + /* 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) { + debugf(" => soft closed"); + return -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); + debugf(" => hard closed"); + return -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 (_w5500_sockintr_ch_recv(&socket->write_ch)) { + case SOCKINTR_SEND_OK: + debugf(" => sent %zu", freesize); + done += freesize; + break; + case SOCKINTR_SEND_TIMEOUT: + debugf(" => ACK timeout"); + return -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"); + } + } + debugf(" => send finished"); + return done; +} + +static void w5500_tcp_set_read_deadline(struct _w5500_socket *socket, uint64_t ns) { + debugf("tcp_conn.set_read_deadline(%"PRIu64")", 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 ssize_t 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; + debugf("tcp_conn.read(%zu)", count); + ASSERT_SELF(stream_conn, TCP); + if (count == 0) + return 0; + + struct alarmclock_trigger trigger = {0}; + 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); + debugf(" => soft closed"); + return -NET_ECLOSED; + } + if (socket->read_deadline_ns && socket->read_deadline_ns <= LO_CALL(bootclock, get_time_ns)) { + LO_CALL(bootclock, del_trigger, &trigger); + debugf(" => recv timeout"); + return -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); + debugf(" => hard closed"); + return -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); + debugf(" => EOF"); + return 0; + } + + cr_mutex_unlock(&chip->mu); + cr_sema_wait(&socket->read_sema); + } + assert(avail); + debugf(" => received %"PRIu16" bytes", avail); + 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 avail; +} + +static int w5500_tcp_close_inner(struct _w5500_socket *socket, bool rd, bool wr) { + debugf("tcp_conn.close(rd=%s, wr=%s)", rd ? "true" : "false", wr ? "true" : "false"); + 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 0; +} + +static int w5500_tcp_close(struct _w5500_socket *socket) { return w5500_tcp_close_inner(socket, true, true); } +static int w5500_tcp_close_read(struct _w5500_socket *socket) { return w5500_tcp_close_inner(socket, true, false); } +static int w5500_tcp_close_write(struct _w5500_socket *socket) { return w5500_tcp_close_inner(socket, false, true); } + +/* udp_conn methods ***********************************************************/ + +static ssize_t w5500_udp_sendto(struct _w5500_socket *socket, void *buf, size_t count, + struct net_ip4_addr node, uint16_t port) { + debugf("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) { + debugf(" => msg too large"); + return -NET_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); + debugf(" => closed"); + return -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 (_w5500_sockintr_ch_recv(&socket->write_ch)) { + case SOCKINTR_SEND_OK: + debugf(" => sent"); + return count; + case SOCKINTR_SEND_TIMEOUT: + debugf(" => ARP timeout"); + return -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) { + debugf("udp_conn.set_recv_deadline(%"PRIu64")", 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 ssize_t w5500_udp_recvfrom(struct _w5500_socket *socket, void *buf, size_t count, + struct net_ip4_addr *ret_node, uint16_t *ret_port) { + debugf("udp_conn.recvfrom()"); + ASSERT_SELF(packet_conn, UDP); + assert(buf); + assert(count); + + struct alarmclock_trigger trigger = {0}; + 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); + debugf(" => recv timeout"); + return -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); + debugf(" => hard closed"); + return -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]); + debugf(" => received %"PRIu16" bytes%s", len, 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 len; +} + +static int w5500_udp_close(struct _w5500_socket *socket) { + debugf("udp_conn.close()"); + ASSERT_SELF(packet_conn, UDP); + + w5500_socket_close(socket); + w5500_free_socket(chip, socket); + return 0; +} diff --git a/libhw_cr/w5500_ll.h b/libhw_cr/w5500_ll.h new file mode 100644 index 0000000..2506cd2 --- /dev/null +++ b/libhw_cr/w5500_ll.h @@ -0,0 +1,426 @@ +/* libhw_cr/w5500_ll.h - Low-level header library for the WIZnet W5500 chip + * + * Based entirely on the W5500 datasheet, v1.1.0. + * https://docs.wiznet.io/img/products/w5500/W5500_ds_v110e.pdf + * + * Copyright (C) 2024-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 <alloca.h> /* for alloca() */ +#include <stdint.h> /* for uint{n}_t */ +#include <string.h> /* for memcmp() */ + +#include <libmisc/assert.h> /* for assert(), 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 */ + +#if CONFIG_W5500_LL_DEBUG +static char *_ctl_block_part_strs[] = { + "RES", + "REG", + "TX", + "RX", +}; +#define PRI_ctl_block "CTL_BLOCK_SOCK(%d, %s)" +#define ARG_ctl_block(b) (((b)>>5) & 0b111), _ctl_block_part_strs[((b)>>3)&0b11] +#endif + +/* Even though SPI is a full-duplex protocol, the W5500's spiframe on top of it is only half-duplex. + * Lame. */ + +static inline void +#if CONFIG_W5500_LL_DEBUG +#define w5500ll_writev(...) _w5500ll_writev(__func__, __VA_ARGS__) +_w5500ll_writev(const char *func, +#else +w5500ll_writev( +#endif + 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 + n_debugf(W5500_LL, + "%s(): w5500ll_write(spidev, addr=%#04x, block="PRI_ctl_block", iov, iovcnt=%d)", + func, addr, ARG_ctl_block(block), 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 = alloca(sizeof(struct duplex_iovec)*inner_cnt); + 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); +} + +static inline void +#if CONFIG_W5500_LL_DEBUG +#define w5500ll_readv(...) _w5500ll_read(__func__, __VA_ARGS__) +_w5500ll_readv(const char *func, +#else +w5500ll_readv( +#endif + 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 + n_debugf(W5500_LL, + "%s(): w5500ll_read(spidev, addr=%#04x, block="PRI_ctl_block", iov, iovcnt=%d)", + func, addr, ARG_ctl_block(block), 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 = alloca(sizeof(struct duplex_iovec)*inner_cnt); + 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); +} + +/* 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) + +/******************************************************************************/ + +#define w5500ll_write_reg(spidev, blockid, blocktyp, field, val) do { \ + typeof((blocktyp){}.field) lval = val; \ + w5500ll_writev(spidev, \ + offsetof(blocktyp, field), \ + blockid, \ + &((struct iovec){ \ + &lval, \ + 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". */ +#define w5500ll_read_reg(spidev, blockid, blocktyp, field) ({ \ + typeof((blocktyp){}.field) val; \ + w5500ll_readv(spidev, \ + offsetof(blocktyp, field), \ + blockid, \ + &((struct iovec){ \ + .iov_base = &val, \ + .iov_len = sizeof(val), \ + }), \ + 1, 0); \ + if (sizeof(val) > 1) \ + for (;;) { \ + typeof(val) val2; \ + w5500ll_readv(spidev, \ + offsetof(blocktyp, field), \ + blockid, \ + &((struct iovec){ \ + .iov_base = &val2, \ + .iov_len = sizeof(val), \ + }), \ + 1, 0); \ + if (memcmp(&val2, &val, sizeof(val)) == 0) \ + break; \ + val = val2; \ + } \ + val; \ + }) + +#endif /* _LIBHW_CR_W5500_LL_H_ */ |