From d1e3188dc23bd140ad095c6a6ee83a62044c33cc Mon Sep 17 00:00:00 2001 From: Nicholas Wiersma Date: Fri, 29 Aug 2025 17:40:52 +0200 Subject: [PATCH 1/2] feat: add w5500 driver --- examples/w5500/main.go | 39 ++++ smoketest.sh | 1 + w5500/debug.go | 15 ++ w5500/io.go | 116 ++++++++++ w5500/netdev.go | 500 +++++++++++++++++++++++++++++++++++++++++ w5500/registers.go | 88 ++++++++ w5500/w5500.go | 262 +++++++++++++++++++++ 7 files changed, 1021 insertions(+) create mode 100644 examples/w5500/main.go create mode 100644 w5500/debug.go create mode 100644 w5500/io.go create mode 100644 w5500/netdev.go create mode 100644 w5500/registers.go create mode 100644 w5500/w5500.go diff --git a/examples/w5500/main.go b/examples/w5500/main.go new file mode 100644 index 000000000..048748505 --- /dev/null +++ b/examples/w5500/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "machine" + "net" + "net/netip" + "time" + + "tinygo.org/x/drivers/netdev" + "tinygo.org/x/drivers/w5500" +) + +func main() { + machine.SPI0.Configure(machine.SPIConfig{ + Frequency: 33 * machine.MHz, + }) + machine.GPIO17.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + eth := w5500.New() + eth.Configure(w5500.Config{ + SPI: machine.SPI0, + CS: machine.GPIO17.Set, + MAC: net.HardwareAddr{0xee, 0xbe, 0xe9, 0xa9, 0xb6, 0x4f}, + IP: netip.AddrFrom4([4]byte{192, 168, 1, 2}), + SubnetMask: netip.AddrFrom4([4]byte{255, 255, 255, 0}), + Gateway: netip.AddrFrom4([4]byte{192, 168, 1, 1}), + }) + netdev.UseNetdev(eth) + + for { + if eth.LinkStatus() != w5500.LinkStatusUp { + println("Waiting for link to be up") + + time.Sleep(1 * time.Second) + continue + } + break + } +} diff --git a/smoketest.sh b/smoketest.sh index b0035988f..a037a3b41 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -144,6 +144,7 @@ tinygo build -size short -o ./build/test.hex -target=pico ./examples/tmc5160/mai tinygo build -size short -o ./build/test.uf2 -target=nicenano ./examples/sharpmem/main.go tinygo build -size short -o ./build/test.hex -target=feather-nrf52840 ./examples/max6675/main.go tinygo build -size short -o ./build/test.hex -target=pico ./examples/ens160/main.go +tinygo build -size short -o ./build/test.hex -target=pico ./examples/w5500/main.go # network examples (espat) tinygo build -size short -o ./build/test.hex -target=challenger-rp2040 ./examples/net/ntpclient/ # network examples (wifinina) diff --git a/w5500/debug.go b/w5500/debug.go new file mode 100644 index 000000000..86b5998e4 --- /dev/null +++ b/w5500/debug.go @@ -0,0 +1,15 @@ +package w5500 + +type debug uint8 + +const ( + debugNetdev debug = 1 << iota // show netdev entry points + debugDetail // show chatty w5500 cmds + + debugOff = 0 + debugAll = debugNetdev | debugDetail +) + +func debugging(want debug) bool { + return (_debug & want) != 0 +} diff --git a/w5500/io.go b/w5500/io.go new file mode 100644 index 000000000..06e29e01c --- /dev/null +++ b/w5500/io.go @@ -0,0 +1,116 @@ +package w5500 + +import ( + "fmt" + "time" +) + +func (d *Device) irqPoll(sockn uint8, state uint8, deadline time.Time) uint8 { + waitTime := 500 * time.Microsecond + for { + if !deadline.IsZero() && time.Now().After(deadline) { + if debugging(debugDetail) { + fmt.Println(time.Now(), deadline) + fmt.Printf("[Socket %d] Polling for IRQ %08b timed out.\r\n", sockn, state) + } + + // If a deadline is set and it has passed, return 0. + return sockIntUnknown + } + + irq := d.readByte(sockInt, sockAddr(sockn)) & 0b00011111 + if got := irq & state; got != 0 { + // Acknowledge the interrupt. + d.writeByte(sockInt, sockAddr(sockn), got) + + if debugging(debugDetail) { + fmt.Printf("[Socket %d] Got IRQ %08b\r\n", sockn, got) + } + + return got + } + + d.mu.Unlock() + + time.Sleep(waitTime) + + // Exponential backoff for polling. + waitTime *= 2 + if waitTime > 10*time.Millisecond { + waitTime = 10 * time.Millisecond + } + + d.mu.Lock() + } +} + +func (d *Device) read(addr uint16, bsb uint8, p []byte) { + d.cs(false) + if len(p) == 0 { + return + } + + d.sendReadHeader(addr, bsb) + _ = d.bus.Tx(nil, p) + d.cs(true) +} + +func (d *Device) readUint16(addr uint16, bsb uint8) uint16 { + d.cs(false) + d.sendReadHeader(addr, bsb) + buf := d.cmdBuf + _ = d.bus.Tx(nil, buf[:2]) + d.cs(true) + return uint16(buf[1]) | uint16(buf[0])<<8 +} + +func (d *Device) readByte(addr uint16, bsb uint8) byte { + d.cs(false) + d.sendReadHeader(addr, bsb) + r, _ := d.bus.Transfer(byte(0)) + d.cs(true) + return r +} + +func (d *Device) write(addr uint16, bsb uint8, p []byte) { + d.cs(false) + if len(p) == 0 { + return + } + d.sendWriteHeader(addr, bsb) + _ = d.bus.Tx(p, nil) + d.cs(true) +} + +func (d *Device) writeUint16(addr uint16, bsb uint8, v uint16) { + d.cs(false) + d.sendWriteHeader(addr, bsb) + buf := d.cmdBuf + buf[0] = byte(v >> 8) + buf[1] = byte(v & 0xff) + _ = d.bus.Tx(buf[:2], nil) + d.cs(true) +} + +func (d *Device) writeByte(addr uint16, bsb uint8, b byte) { + d.cs(false) + d.sendWriteHeader(addr, bsb) + _, _ = d.bus.Transfer(b) + d.cs(true) +} + +func (d *Device) sendReadHeader(addr uint16, bsb uint8) { + buf := d.cmdBuf + buf[0] = byte(addr >> 8) + buf[1] = byte(addr & 0xff) + buf[2] = bsb<<3 | 0b000 + _ = d.bus.Tx(buf, nil) +} + +func (d *Device) sendWriteHeader(addr uint16, bsb uint8) { + buf := d.cmdBuf + buf[0] = byte(addr >> 8) + buf[1] = byte(addr & 0xff) + buf[2] = bsb<<3 | 0b100 + _ = d.bus.Tx(buf, nil) +} diff --git a/w5500/netdev.go b/w5500/netdev.go new file mode 100644 index 000000000..5be81df4c --- /dev/null +++ b/w5500/netdev.go @@ -0,0 +1,500 @@ +package w5500 + +import ( + "fmt" + "net" + "net/netip" + "os" + "runtime" + "time" + + "tinygo.org/x/drivers/netdev" +) + +type socket struct { + sockn uint8 + protocol uint8 + port uint16 + inUse bool + closed bool +} + +func (s *socket) setSockn(n int) *socket { + s.sockn = uint8(n) + return s +} + +func (s *socket) setProtocol(proto byte) *socket { + s.protocol = proto + return s +} + +func (s *socket) setPort(port uint16) *socket { + s.port = port + return s +} + +func (s *socket) setInUse(inUse bool) *socket { + s.inUse = inUse + return s +} + +func (s *socket) setClosed(closed bool) *socket { + s.closed = closed + return s +} + +func (s *socket) reset() { + s.protocol = 0 + s.port = 0 + s.inUse = false + s.closed = false +} + +// GetHostByName resolves the given host name to an IP address. +func (d *Device) GetHostByName(name string) (netip.Addr, error) { + d.mu.Lock() + dns := d.dns + d.mu.Unlock() + + if dns == nil { + return netip.Addr{}, netdev.ErrNotSupported + } + return dns(name) +} + +func (d *Device) Socket(domain int, stype int, protocol int) (int, error) { + if debugging(debugNetdev) { + fmt.Printf("[Socket] domain: %d, type: %d, protocol: %d\r\n", + domain, stype, protocol) + } + + if domain != netdev.AF_INET { + return -1, netdev.ErrFamilyNotSupported + } + switch { + case stype == netdev.SOCK_STREAM && protocol == netdev.IPPROTO_TCP: + case stype == netdev.SOCK_DGRAM && protocol == netdev.IPPROTO_UDP: + default: + return -1, fmt.Errorf("unsupported socket type %d and protocol %d", stype, protocol) + } + + var proto byte + switch protocol { + case netdev.IPPROTO_TCP: + proto = 1 // TCP + case netdev.IPPROTO_UDP: + proto = 2 // UDP + default: + return -1, netdev.ErrNotSupported + } + + d.mu.Lock() + defer d.mu.Unlock() + + sockfd, sock, err := d.nextSocket() + if err != nil { + return -1, err + } + + d.openSocket(sock.sockn, proto) + + sock.setProtocol(proto).setInUse(true) + return sockfd, nil +} + +func (d *Device) openSocket(sockn uint8, proto byte) { + d.writeByte(sockMode, sockAddr(sockn), proto&0x0F) +} + +func (d *Device) Bind(sockfd int, ip netip.AddrPort) error { + if debugging(debugNetdev) { + fmt.Printf("[Bind] sockfd: %d, addr: %s:%d\r\n", sockfd, ip.Addr(), ip.Port()) + } + + // The IP address is irrelevant. The configured ip will always be used. + port := ip.Port() + if port < 1 || port > 65535 { + return fmt.Errorf("invalid port number: %d", port) + } + + d.mu.Lock() + defer d.mu.Unlock() + + sock, err := d.socket(sockfd) + if err != nil { + return err + } + + if err = d.bindSocket(sock.sockn, port); err != nil { + return fmt.Errorf("could not set socket port: %w", err) + } + + sock.setPort(port) + return nil +} + +func (d *Device) bindSocket(sockn uint8, port uint16) error { + d.writeUint16(sockSrcPort, sockAddr(sockn), port) + d.socketSendCmd(sockn, sockCmdOpen) + if d.sockStatus(sockn) == sockStatusClosed { + return fmt.Errorf("socket %d is closed after binding", sockn) + } + return nil +} + +// SetSockOpt sets the socket option for the given socket file descriptor. +// It is not supported by the W5500, so it always returns an error. +func (d *Device) SetSockOpt(int, int, int, any) error { + return netdev.ErrNotSupported +} + +// Connect establishes a connection to the specified host and port or ip and port. +// +// If the host is an empty string, it will use the provided ip address and port, +// otherwise it will resolve the host name to an IP address. +func (d *Device) Connect(sockfd int, host string, ip netip.AddrPort) error { + if debugging(debugNetdev) { + if host == "" { + fmt.Printf("[Connect] sockfd: %d, addr: %s\r\n", sockfd, ip) + } else { + fmt.Printf("[Connect] sockfd: %d, host: %s:%d\r\n", sockfd, host, ip.Port()) + } + } + + destIP := ip.Addr() + if host != "" { + var err error + destIP, err = d.GetHostByName(host) + if err != nil { + return fmt.Errorf("could not resolve host %s: %w", host, err) + } + } + if !destIP.IsValid() || !destIP.Is4() { + return fmt.Errorf("invalid destination IP address: %s", destIP) + } + port := ip.Port() + if port < 1 || port > 65535 { + return fmt.Errorf("invalid destination port number: %d", port) + } + + d.mu.Lock() + defer d.mu.Unlock() + + sock, err := d.socket(sockfd) + if err != nil { + return err + } + + d.write(sockDestIP, sockAddr(sock.sockn), destIP.AsSlice()) + d.writeUint16(sockDestPort, sockAddr(sock.sockn), port) + d.socketSendCmd(sock.sockn, sockCmdOpen) + return nil +} + +// Listen sets the socket to listen for incoming connections on the specified socket file descriptor. +// +// The backlog parameter is ignored, as the W5500 does not support it. +func (d *Device) Listen(sockfd int, _ int) error { + if debugging(debugNetdev) { + fmt.Printf("[Listen] sockfd: %d\r\n", sockfd) + } + + d.mu.Lock() + defer d.mu.Unlock() + + sock, err := d.socket(sockfd) + if err != nil { + return err + } + + if sock.protocol != 1 { // Only TCP sockets can listen + return fmt.Errorf("socket %d is not a TCP socket", sockfd) + } + + if err = d.listen(sock.sockn); err != nil { + return fmt.Errorf("could not send listen command: %w", err) + } + return nil +} + +func (d *Device) listen(sockn uint8) error { + state := d.sockStatus(sockn) + if state != sockStatusInit { + return fmt.Errorf("socket %d is not in the initial state: got %d", sockn, state) + } + d.socketSendCmd(sockn, sockCmdListen) + return nil +} + +// Accept waits for an incoming connection on the specified socket file descriptor. +func (d *Device) Accept(sockfd int) (int, netip.AddrPort, error) { + if debugging(debugNetdev) { + fmt.Printf("[Accept] sockfd: %d\r\n", sockfd) + } + + d.mu.Lock() + defer d.mu.Unlock() + + lsock, err := d.socket(sockfd) + if err != nil { + return -1, netip.AddrPort{}, fmt.Errorf("could not get socket: %w", err) + } + + if err = d.waitForEstablished(lsock.sockn); err != nil { + return -1, netip.AddrPort{}, err + } + + // Acquire a new socket for the listening connection. + csockfd, csock, err := d.nextSocket() + if err != nil { + return -1, netip.AddrPort{}, err + } + // Swap the socket numbers of the client and listening sockets. + lsock.sockn, csock.sockn = csock.sockn, lsock.sockn + + // Rebind the listening socket to the local address and port and start listening. + d.openSocket(lsock.sockn, lsock.protocol) + if err = d.bindSocket(lsock.sockn, lsock.port); err != nil { + return -1, netip.AddrPort{}, fmt.Errorf("could not bind listening socket: %w", err) + } + if err = d.listen(lsock.sockn); err != nil { + return -1, netip.AddrPort{}, fmt.Errorf("could not set listening socket: %w", err) + } + + csock.setInUse(true) + + if debugging(debugNetdev) { + fmt.Printf("[Accepted] sockfd: %d\r\n", csockfd) + } + + remoteIP := d.remoteIP(csock.sockn) + return csockfd, remoteIP, nil +} + +func (d *Device) waitForEstablished(sockn uint8) error { + for { + status := d.sockStatus(sockn) + switch status { + case sockStatusEstablished: + return nil + case sockStatusClosed: + return net.ErrClosed + case sockStatusCloseWait: + // The server closed the connection, so we need to reset the socket + // and set it to listen again. + if err := d.listen(sockn); err != nil { + return fmt.Errorf("could not set socket to listen: %w", err) + } + break + } + + d.irqPoll(sockn, sockIntConnect|sockIntDisconnect, time.Time{}) + } +} + +func (d *Device) remoteIP(sockn uint8) netip.AddrPort { + var rip [4]byte + d.read(sockDestIP, sockAddr(sockn), rip[:]) + + var rport [2]byte + d.read(sockDestPort, sockAddr(sockn), rport[:]) + + return netip.AddrPortFrom(netip.AddrFrom4(rip), uint16(rport[0])<<8|uint16(rport[1])) +} + +// Send sends data to the socket with the given file descriptor. +// It blocks until all data is sent or the deadline is reached. +func (d *Device) Send(sockfd int, buf []byte, _ int, deadline time.Time) (int, error) { + if debugging(debugNetdev) { + fmt.Printf("[Send] sockfd: %d, len(buf): %d\r\n", + sockfd, len(buf)) + } + + bufLen := len(buf) + if bufLen <= d.maxSockSize { + // Fast path for small buffers. + return d.sendChunk(sockfd, buf, deadline) + } + + var n int + for i := 0; i < bufLen; i += d.maxSockSize { + end := i + d.maxSockSize + if end > bufLen { + end = bufLen + } + + sent, err := d.sendChunk(sockfd, buf[i:end], deadline) + if err != nil { + return n, fmt.Errorf("could not send chunk: %w", err) + } + n += sent + } + return n, nil +} + +func (d *Device) sendChunk(sockfd int, buf []byte, deadline time.Time) (int, error) { + d.mu.Lock() + defer d.mu.Unlock() + + sock, err := d.socket(sockfd) + if err != nil { + return 0, fmt.Errorf("could not get socket: %w", err) + } + if sock.closed { + return 0, os.ErrClosed + } + + bufLen := uint16(len(buf)) + if err = d.waitForFreeBuffer(sock.sockn, bufLen, deadline); err != nil { + return 0, err + } + + sendPtr := d.readUint16(sockTXWritePtr, sockAddr(sock.sockn)) + + d.write(sendPtr, sock.sockn<<2|0b10, buf) + d.writeUint16(sockTXWritePtr, sockAddr(sock.sockn), sendPtr+bufLen) + d.writeByte(sockCmd, sockAddr(sock.sockn), sockCmdSend) + + irq := d.irqPoll(sock.sockn, sockIntSendOK|sockIntDisconnect|sockIntTimeout, deadline) + switch { + case irq == sockIntUnknown: + return 0, os.ErrDeadlineExceeded + case irq&sockIntDisconnect != 0: + sock.setClosed(true) + return 0, net.ErrClosed + case irq&sockIntTimeout != 0: + return 0, netdev.ErrTimeout + default: + return int(bufLen), nil + } +} + +func (d *Device) waitForFreeBuffer(sockn uint8, len uint16, deadline time.Time) error { + for { + freeSize := d.readUint16(sockTXFreeSize, sockAddr(sockn)) + if freeSize >= len { + return nil + } + + if !deadline.IsZero() && time.Now().After(deadline) { + return netdev.ErrTimeout + } + + status := d.sockStatus(sockn) + switch status { + case sockStatusEstablished, sockStatusCloseWait: + default: + return fmt.Errorf("socket is not in a valid state for sending data: %d", status) + } + + d.mu.Unlock() + + time.Sleep(time.Millisecond) + + d.mu.Lock() + } +} + +// Recv reads data from the socket with the given file descriptor into the provided buffer. +// It blocks until data is available or the deadline is reached. +func (d *Device) Recv(sockfd int, buf []byte, _ int, deadline time.Time) (int, error) { + if debugging(debugNetdev) { + fmt.Printf("[Recv] sockfd: %d, len(buf): %d\r\n", + sockfd, len(buf)) + } + + d.mu.Lock() + defer d.mu.Unlock() + + sock, err := d.socket(sockfd) + if err != nil { + return 0, fmt.Errorf("could not get socket: %w", err) + } + if sock.closed { + return 0, os.ErrClosed + } + + size, err := d.waitForData(sock, deadline) + if err != nil { + return 0, err + } + + recvPtr := d.readUint16(sockRXReadPtr, sockAddr(sock.sockn)) + + buf = buf[:min(size, len(buf))] + d.read(recvPtr, sock.sockn<<2|0b00011, buf) + d.writeUint16(sockRXReadPtr, sockAddr(sock.sockn), recvPtr+uint16(len(buf))) + d.socketSendCmd(sock.sockn, sockCmdRecv) + + return len(buf), nil +} + +func (d *Device) waitForData(sock *socket, deadline time.Time) (int, error) { + for { + recvdSize := d.readUint16(sockRXReceivedSize, sockAddr(sock.sockn)) + if recvdSize > 0 { + return int(recvdSize), nil + } + + irq := d.irqPoll(sock.sockn, sockIntReceive|sockIntDisconnect, deadline) + switch { + case irq == sockIntUnknown: + return 0, os.ErrDeadlineExceeded + case irq&sockIntDisconnect != 0: + sock.setClosed(true) + return 0, net.ErrClosed + } + } +} + +// Close closes the socket with the given file descriptor. +func (d *Device) Close(sockfd int) error { + if debugging(debugNetdev) { + fmt.Printf("[Close] sockfd: %d\r\n", sockfd) + } + + d.mu.Lock() + defer d.mu.Unlock() + + sock, err := d.socket(sockfd) + if err != nil { + return err + } + + d.socketSendCmd(sock.sockn, sockCmdClose) + sock.reset() + return nil +} + +func (d *Device) nextSocket() (int, *socket, error) { + for i, sock := range d.sockets { + if sock.inUse { + continue + } + return i, sock, nil + } + return -1, nil, netdev.ErrNoMoreSockets +} + +func (d *Device) socket(sockfd int) (*socket, error) { + if sockfd < 0 || sockfd >= len(d.sockets) { + return nil, netdev.ErrInvalidSocketFd + } + return d.sockets[sockfd], nil +} + +func (d *Device) socketSendCmd(sockn uint8, cmd byte) { + d.writeByte(sockCmd, sockAddr(sockn), cmd) + for d.readByte(sockCmd, sockAddr(sockn)) != 0 { + runtime.Gosched() + } +} + +func (d *Device) sockStatus(sockn uint8) int { + return int(d.readByte(sockStatus, sockAddr(sockn))) +} + +func sockAddr(sockn uint8) uint8 { + return sockn<<2 | 0b0001 +} diff --git a/w5500/registers.go b/w5500/registers.go new file mode 100644 index 000000000..0ff1b09c7 --- /dev/null +++ b/w5500/registers.go @@ -0,0 +1,88 @@ +package w5500 + +// Common Registers. +const ( + regMode = 0x0000 + regGatewayAddr = 0x0001 + regSubnetMask = 0x0005 + regMAC = 0x0009 + regIPAddr = 0x000F + regIntLevel = 0x0013 + regInt = 0x0015 + regIntMask = 0x0016 + regSockInt = 0x0017 + regSockIntMask = 0x0018 + regRetryTime = 0x0019 + regRetryN = 0x001B + // ... PPP registers, not needed + regPHYCfg = 0x002E + regChipVer = 0x0039 +) + +// Socket Registers. +const ( + sockMode = 0x0000 + sockCmd = 0x0001 + sockInt = 0x0002 + sockStatus = 0x0003 + sockSrcPort = 0x0004 + sockDestMAC = 0x0006 + sockDestIP = 0x000C + sockDestPort = 0x0010 + sockMaxSegSize = 0x0012 + sockIPTOS = 0x0015 + sockIPTTL = 0x0016 + sockRXBUFSize = 0x001E + sockTXBUFSize = 0x001F + sockTXFreeSize = 0x0020 + sockTXReadPtr = 0x0022 + sockTXWritePtr = 0x0024 + sockRXReceivedSize = 0x0026 + sockRXReadPtr = 0x0028 + sockRXWritePtr = 0x002A + sockIntMask = 0x002C + sockKeepInt = 0x002F +) + +// Socket Commands. +const ( + sockCmdOpen = 0x01 + sockCmdClose = 0x10 + sockCmdListen = 0x02 + sockCmdConnect = 0x04 + sockCmdDisconnect = 0x08 + sockCmdSend = 0x20 + sockCmdSendMacRaw = 0x21 + sockCmdSendKeep = 0x22 + sockCmdRecv = 0x40 +) + +// Socket Statuses. +const ( + sockStatusClosed = 0x00 + sockStatusInit = 0x13 + sockStatusListen = 0x14 + sockStatusEstablished = 0x17 + sockStatusCloseWait = 0x1C + sockStatusUdp = 0x22 + sockStatusMacRaw = 0x42 + // Temporary TCP states + sockStatusSynSent = 0x15 + sockStatusSynRecv = 0x16 + sockStatusFinWait = 0x18 + sockStatusClosing = 0x1A + sockStatusTimeWait = 0x1B + sockStatusLastAck = 0x1D + sockStatusUnknown = 0xFF +) + +// Socket Interrupts. +const ( + sockIntConnect uint8 = 1 << iota + sockIntDisconnect + sockIntReceive + sockIntTimeout + sockIntSendOK + + sockIntUnknown uint8 = 0 +) diff --git a/w5500/w5500.go b/w5500/w5500.go new file mode 100644 index 000000000..d14f28eb4 --- /dev/null +++ b/w5500/w5500.go @@ -0,0 +1,262 @@ +package w5500 + +import ( + "fmt" + "net" + "net/netip" + "sync" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/netdev" +) + +var _ netdev.Netdever = &Device{} + +var _debug debug = debugOff + +// Resolver is a function that resolves a hostname to an IP address. +type Resolver func(host string) (netip.Addr, error) + +// PinOutput is a function that sets a pin high or low. +type PinOutput func(level bool) + +// Device is a driver for the W5500 Ethernet controller. +type Device struct { + maxSockets int + maxSockSize int + + mu sync.Mutex + bus drivers.SPI + cs PinOutput + dns Resolver + + sockets []*socket + laddr netip.Addr + + cmdBuf []byte +} + +// New returns a new w5500 driver. +func New() *Device { + return &Device{ + cmdBuf: make([]byte, 3), + } +} + +// Config is the configuration for the device. +// +// The SPI bus must be fully configured. +type Config struct { + SPI drivers.SPI + CS PinOutput + DNS Resolver + + MAC net.HardwareAddr + IP netip.Addr + SubnetMask netip.Addr + Gateway netip.Addr + + // Optional, default is 8. + MaxSockets int +} + +// Configure sets up the device. +// +// MAC address must be provided. The other fields are optional. +func (d *Device) Configure(cfg Config) error { + cfg.CS(true) + + d.mu.Lock() + defer d.mu.Unlock() + + d.bus = cfg.SPI + d.cs = cfg.CS + d.dns = cfg.DNS + + d.reset() + + if err := d.setupSockets(cfg.MaxSockets); err != nil { + return fmt.Errorf("could not setup sockets: %w", err) + } + + // Set the MAC address and IP configuration. + d.write(regMAC, 0, cfg.MAC) + d.write(regIPAddr, 0, cfg.IP.AsSlice()) + d.write(regSubnetMask, 0, cfg.SubnetMask.AsSlice()) + d.write(regGatewayAddr, 0, cfg.Gateway.AsSlice()) + d.laddr = cfg.IP + + return nil +} + +func (d *Device) setupSockets(maxSockets int) error { + if maxSockets == 0 { + maxSockets = 8 // Default to 8 sockets if not specified. + } + switch maxSockets { + case 1, 2, 4, 8: + // Valid socket counts. + default: + return fmt.Errorf("invalid number of sockets: %d, must be one of 1, 2, 4, or 8", maxSockets) + } + + socks := make([]*socket, maxSockets) + for i := range socks { + socks[i] = &socket{ + sockn: uint8(i), + } + } + + d.maxSockets = maxSockets + d.maxSockSize = 16 * 1024 / maxSockets + d.sockets = socks + + // Set the RX and TX buffer sizes for each socket. + for i := 0; i < 8; i++ { + size := byte(d.maxSockSize >> 10) + if i >= maxSockets { + size = 0 + } + + d.writeByte(sockRXBUFSize, sockAddr(uint8(i)), size) + d.writeByte(sockTXBUFSize, sockAddr(uint8(i)), size) + } + + mask := byte(0b11111111) + switch maxSockets { + case 1: + mask = 0b00000001 + case 2: + mask = 0b00000011 + case 4: + mask = 0b00001111 + } + d.writeByte(regSockIntMask, 0, mask) + d.writeByte(regIntMask, 0, 0) + return nil +} + +func (d *Device) findSocket(sockn uint8) *socket { + for _, sock := range d.sockets { + if sock.sockn == sockn { + return sock + } + } + // This should not be possible + panic(fmt.Sprintf("could not find socket with sockn %d", sockn)) +} + +// Reset performs a soft reset. +func (d *Device) Reset() { + d.mu.Lock() + defer d.mu.Unlock() + + d.reset() +} + +func (d *Device) reset() { + // RST is bit 7 of regMode. + d.writeByte(regMode, 0, 0x80) +} + +// GetHardwareAddr returns the hardware address of the device. +func (d *Device) GetHardwareAddr() (net.HardwareAddr, error) { + if debugging(debugNetdev) { + fmt.Printf("[GetHardwareAddr]\r\n") + } + + d.mu.Lock() + defer d.mu.Unlock() + + mac := make([]byte, 6) + d.read(regMAC, 0, mac) + return mac, nil +} + +// Addr returns the IP address of the device. +func (d *Device) Addr() (netip.Addr, error) { + d.mu.Lock() + defer d.mu.Unlock() + + var ip [4]byte + d.read(regIPAddr, 0, ip[:]) + return netip.AddrFrom4(ip), nil +} + +// SetAddr sets the IP address of the device. +// +// The IP address must be a valid IPv4 address. +func (d *Device) SetAddr(ip netip.Addr) error { + if err := d.setAddress(regIPAddr, ip); err != nil { + return fmt.Errorf("could not set IP address: %w", err) + } + + d.mu.Lock() + defer d.mu.Unlock() + + d.laddr = ip + return nil +} + +// SetSubnetMask sets the subnet mask of the device. +// +// The subnet mask must be a valid IPv4 address. +// It is not checked if the subnet mask is valid for the device's IP address. +func (d *Device) SetSubnetMask(mask netip.Addr) error { + return d.setAddress(regSubnetMask, mask) +} + +// SetGateway sets the gateway address of the device. +// +// The gateway must be a valid IPv4 address. +// It is not checked if the gateway is in the same subnet as the device. +func (d *Device) SetGateway(gateway netip.Addr) error { + return d.setAddress(regGatewayAddr, gateway) +} + +func (d *Device) setAddress(addr uint16, ip netip.Addr) error { + if !ip.IsValid() || !ip.Is4() { + return fmt.Errorf("invalid IP address: %s", ip) + } + + d.mu.Lock() + defer d.mu.Unlock() + + d.write(addr, 0, ip.AsSlice()) + return nil +} + +// LinkStatus is the link status of the device. +type LinkStatus = uint8 + +// LinkStatus values. +const ( + LinkStatusDown LinkStatus = iota + LinkStatusUp +) + +// LinkStatus returns the current link status of the device. +func (d *Device) LinkStatus() LinkStatus { + d.mu.Lock() + defer d.mu.Unlock() + + return d.readByte(regPHYCfg, 0) & 0b00000001 +} + +// LinkInfo returns the current link information of the device. +func (d *Device) LinkInfo() string { + d.mu.Lock() + defer d.mu.Unlock() + + linkInfo := d.readByte(regPHYCfg, 0) & 0b00000110 + + speed := "10Mbps" + if linkInfo&0b00000010 != 0 { + speed = "100Mbps" + } + duplex := "Half Duplex" + if linkInfo&0b00000100 != 0 { + duplex = "Full Duplex" + } + return speed + " " + duplex +} From 102798a8cb3042b7abcbc7f9140de281727512c1 Mon Sep 17 00:00:00 2001 From: Nicholas Wiersma Date: Tue, 23 Sep 2025 04:32:27 +0200 Subject: [PATCH 2/2] fix: cr suggestions --- examples/w5500/main.go | 4 +--- w5500/io.go | 4 ++-- w5500/w5500.go | 13 +++++-------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/examples/w5500/main.go b/examples/w5500/main.go index 048748505..782965087 100644 --- a/examples/w5500/main.go +++ b/examples/w5500/main.go @@ -16,10 +16,8 @@ func main() { }) machine.GPIO17.Configure(machine.PinConfig{Mode: machine.PinOutput}) - eth := w5500.New() + eth := w5500.New(machine.SPI0, machine.GPIO17.Set) eth.Configure(w5500.Config{ - SPI: machine.SPI0, - CS: machine.GPIO17.Set, MAC: net.HardwareAddr{0xee, 0xbe, 0xe9, 0xa9, 0xb6, 0x4f}, IP: netip.AddrFrom4([4]byte{192, 168, 1, 2}), SubnetMask: netip.AddrFrom4([4]byte{255, 255, 255, 0}), diff --git a/w5500/io.go b/w5500/io.go index 06e29e01c..3c80a3a61 100644 --- a/w5500/io.go +++ b/w5500/io.go @@ -104,7 +104,7 @@ func (d *Device) sendReadHeader(addr uint16, bsb uint8) { buf[0] = byte(addr >> 8) buf[1] = byte(addr & 0xff) buf[2] = bsb<<3 | 0b000 - _ = d.bus.Tx(buf, nil) + _ = d.bus.Tx(buf[:], nil) } func (d *Device) sendWriteHeader(addr uint16, bsb uint8) { @@ -112,5 +112,5 @@ func (d *Device) sendWriteHeader(addr uint16, bsb uint8) { buf[0] = byte(addr >> 8) buf[1] = byte(addr & 0xff) buf[2] = bsb<<3 | 0b100 - _ = d.bus.Tx(buf, nil) + _ = d.bus.Tx(buf[:], nil) } diff --git a/w5500/w5500.go b/w5500/w5500.go index d14f28eb4..3269289c0 100644 --- a/w5500/w5500.go +++ b/w5500/w5500.go @@ -33,13 +33,14 @@ type Device struct { sockets []*socket laddr netip.Addr - cmdBuf []byte + cmdBuf [3]byte } // New returns a new w5500 driver. -func New() *Device { +func New(bus drivers.SPI, cs PinOutput) *Device { return &Device{ - cmdBuf: make([]byte, 3), + bus: bus, + cs: cs, } } @@ -47,8 +48,6 @@ func New() *Device { // // The SPI bus must be fully configured. type Config struct { - SPI drivers.SPI - CS PinOutput DNS Resolver MAC net.HardwareAddr @@ -64,13 +63,11 @@ type Config struct { // // MAC address must be provided. The other fields are optional. func (d *Device) Configure(cfg Config) error { - cfg.CS(true) + d.cs(true) d.mu.Lock() defer d.mu.Unlock() - d.bus = cfg.SPI - d.cs = cfg.CS d.dns = cfg.DNS d.reset()