From 3c5e17423a160919f42f2fd4851fea2b7e977a14 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Mon, 15 Jan 2024 21:06:14 +0100 Subject: [PATCH 001/137] netlink, examples: use 'ninafw' tag instead of individual board tags to simplify maintenence. Also see PR #4085 in the main TinyGo repo Signed-off-by: deadprogram --- examples/net/http-get/main.go | 2 +- examples/net/http-head/main.go | 2 +- examples/net/http-post/main.go | 2 +- examples/net/http-postform/main.go | 2 +- examples/net/mqttclient/natiu/main.go | 2 +- examples/net/mqttclient/paho/main.go | 2 +- examples/net/ntpclient/main.go | 2 +- examples/net/socket/main.go | 2 +- examples/net/tcpclient/main.go | 2 +- examples/net/tcpecho/main.go | 2 +- examples/net/tlsclient/main.go | 2 +- examples/net/webclient/main.go | 2 +- examples/net/webserver/main.go | 2 +- examples/net/websocket/dial/main.go | 2 +- examples/net/websocket/handler/main.go | 2 +- examples/net/webstatic/main.go | 2 +- netlink/probe/wifinina.go | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/examples/net/http-get/main.go b/examples/net/http-get/main.go index 7f8de67..4359f8d 100644 --- a/examples/net/http-get/main.go +++ b/examples/net/http-get/main.go @@ -9,7 +9,7 @@ // examples/net/webclient (for HTTP) // examples/net/tlsclient (for HTTPS) -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal package main diff --git a/examples/net/http-head/main.go b/examples/net/http-head/main.go index 07c1f45..355e9d4 100644 --- a/examples/net/http-head/main.go +++ b/examples/net/http-head/main.go @@ -9,7 +9,7 @@ // examples/net/webclient (for HTTP) // examples/net/tlsclient (for HTTPS) -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal package main diff --git a/examples/net/http-post/main.go b/examples/net/http-post/main.go index d28f62d..8af7516 100644 --- a/examples/net/http-post/main.go +++ b/examples/net/http-post/main.go @@ -9,7 +9,7 @@ // examples/net/webclient (for HTTP) // examples/net/tlsclient (for HTTPS) -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal package main diff --git a/examples/net/http-postform/main.go b/examples/net/http-postform/main.go index e72aac8..03f87f9 100644 --- a/examples/net/http-postform/main.go +++ b/examples/net/http-postform/main.go @@ -9,7 +9,7 @@ // examples/net/webclient (for HTTP) // examples/net/tlsclient (for HTTPS) -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal package main diff --git a/examples/net/mqttclient/natiu/main.go b/examples/net/mqttclient/natiu/main.go index 8d82fe7..5c82811 100644 --- a/examples/net/mqttclient/natiu/main.go +++ b/examples/net/mqttclient/natiu/main.go @@ -4,7 +4,7 @@ // Note: It may be necessary to increase the stack size when using // paho.mqtt.golang. Use the -stack-size=4KB command line option. -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal || challenger_rp2040 +//go:build ninafw || wioterminal || challenger_rp2040 package main diff --git a/examples/net/mqttclient/paho/main.go b/examples/net/mqttclient/paho/main.go index 293e24d..ac39a2c 100644 --- a/examples/net/mqttclient/paho/main.go +++ b/examples/net/mqttclient/paho/main.go @@ -4,7 +4,7 @@ // Note: It may be necessary to increase the stack size when using // paho.mqtt.golang. Use the -stack-size=4KB command line option. -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal || challenger_rp2040 +//go:build ninafw || wioterminal || challenger_rp2040 package main diff --git a/examples/net/ntpclient/main.go b/examples/net/ntpclient/main.go index 6c84193..da54c5d 100644 --- a/examples/net/ntpclient/main.go +++ b/examples/net/ntpclient/main.go @@ -3,7 +3,7 @@ // It creates a UDP connection to request the current time and parse the // response from a NTP server. The system time is set to NTP time. -//go:build pyportal || arduino_nano33 || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal || challenger_rp2040 +//go:build ninafw || wioterminal || challenger_rp2040 package main diff --git a/examples/net/socket/main.go b/examples/net/socket/main.go index cf8bd93..f55b053 100644 --- a/examples/net/socket/main.go +++ b/examples/net/socket/main.go @@ -4,7 +4,7 @@ // // nc -lk 8080 -//go:build pyportal || arduino_nano33 || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal || challenger_rp2040 +//go:build ninafw || wioterminal || challenger_rp2040 package main diff --git a/examples/net/tcpclient/main.go b/examples/net/tcpclient/main.go index 10d2935..9ef9820 100644 --- a/examples/net/tcpclient/main.go +++ b/examples/net/tcpclient/main.go @@ -5,7 +5,7 @@ // // nc -lk 8080 -//go:build pyportal || arduino_nano33 || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal || challenger_rp2040 || pico +//go:build ninafw || wioterminal || challenger_rp2040 || pico package main diff --git a/examples/net/tcpecho/main.go b/examples/net/tcpecho/main.go index 27faf8f..274dc04 100644 --- a/examples/net/tcpecho/main.go +++ b/examples/net/tcpecho/main.go @@ -7,7 +7,7 @@ // // $ nc 10.0.0.2 8080 copy ; cmp file copy -//go:build pyportal || arduino_nano33 || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal package main diff --git a/examples/net/tlsclient/main.go b/examples/net/tlsclient/main.go index 186fdd5..46cd110 100644 --- a/examples/net/tlsclient/main.go +++ b/examples/net/tlsclient/main.go @@ -5,7 +5,7 @@ // // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal package main diff --git a/examples/net/webclient/main.go b/examples/net/webclient/main.go index bb2fe9d..43096bf 100644 --- a/examples/net/webclient/main.go +++ b/examples/net/webclient/main.go @@ -17,7 +17,7 @@ // } // --------------------------------------------------------------------------- -//go:build pyportal || arduino_nano33 || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal package main diff --git a/examples/net/webserver/main.go b/examples/net/webserver/main.go index 84f0fb4..4b7860c 100644 --- a/examples/net/webserver/main.go +++ b/examples/net/webserver/main.go @@ -6,7 +6,7 @@ // Note: It may be necessary to increase the stack size when using "net/http". // Use the -stack-size=4KB command line option. -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal package main diff --git a/examples/net/websocket/dial/main.go b/examples/net/websocket/dial/main.go index 5a7288d..4aa4b8e 100644 --- a/examples/net/websocket/dial/main.go +++ b/examples/net/websocket/dial/main.go @@ -6,7 +6,7 @@ // Note: It may be necessary to increase the stack size when using // "golang.org/x/net/websocket". Use the -stack-size=4KB command line option. -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal package main diff --git a/examples/net/websocket/handler/main.go b/examples/net/websocket/handler/main.go index 709f14c..f03e32c 100644 --- a/examples/net/websocket/handler/main.go +++ b/examples/net/websocket/handler/main.go @@ -6,7 +6,7 @@ // Note: It may be necessary to increase the stack size when using // "golang.org/x/net/websocket". Use the -stack-size=4KB command line option. -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal package main diff --git a/examples/net/webstatic/main.go b/examples/net/webstatic/main.go index 9130822..b2d8bb9 100644 --- a/examples/net/webstatic/main.go +++ b/examples/net/webstatic/main.go @@ -3,7 +3,7 @@ // Note: It may be necessary to increase the stack size when using "net/http". // Use the -stack-size=4KB command line option. -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal package main diff --git a/netlink/probe/wifinina.go b/netlink/probe/wifinina.go index bae9647..0668eee 100644 --- a/netlink/probe/wifinina.go +++ b/netlink/probe/wifinina.go @@ -1,4 +1,4 @@ -//go:build pyportal || arduino_nano33 || nano_rp2040 || metro_m4_airlift || matrixportal_m4 +//go:build ninafw && !arduino_mkrwifi1010 package probe From e7931c6a22c7efd542aeb21a107b20fb5348661a Mon Sep 17 00:00:00 2001 From: Scott Feldman Date: Mon, 22 Jan 2024 13:28:21 -0800 Subject: [PATCH 002/137] wifinina: fix concurrency issues with multiple sockets wifinina driver was not handling concurrent socket connections correctly. When trying to make multiple socket connections, the sockfd returned by the first Socket() call would be the same sockfd returned by subsequent Socket() calls. The problem is the sockfd returned by Socket() isn't used until Connect() (or Appect()). This would result in Connect() trying to use the same sockfd for multiple connections, causing wifinina fw to lock up. The solution in this PR is to create a new sockfd space managed by the driver that gives the app a unique, safe sockfd for each connection. The real underlying sock fd returned by fw is set on Connect() (or Accept()), and mapped back to the apps sockfd using a map: sockets map[int]*Socket // keyed by sockfd Where Socket has a reference to the fw sock: type Socket struct { protocol int clientConnected bool laddr netip.AddrPort // Set in Bind() raddr netip.AddrPort // Set in Connect() sock // Device socket, as returned from w.getSocket() } --- netdev/netdev.go | 1 + wifinina/wifinina.go | 225 +++++++++++++++++++++++++------------------ 2 files changed, 133 insertions(+), 93 deletions(-) diff --git a/netdev/netdev.go b/netdev/netdev.go index 229b50a..01a8bf8 100644 --- a/netdev/netdev.go +++ b/netdev/netdev.go @@ -39,6 +39,7 @@ var ( ErrNoMoreSockets = errors.New("No more sockets") ErrClosingSocket = errors.New("Error closing socket") ErrNotSupported = errors.New("Not supported") + ErrInvalidSocketFd = errors.New("Invalid socket fd") ) // Duplicate of non-exported net.errTimeout diff --git a/wifinina/wifinina.go b/wifinina/wifinina.go index c0c3a66..4f0eea1 100644 --- a/wifinina/wifinina.go +++ b/wifinina/wifinina.go @@ -163,11 +163,12 @@ type encryptionType uint8 type sock uint8 type hwerr uint8 -type socket struct { - protocol int - laddr netip.AddrPort // Set in Bind() - raddr netip.AddrPort // Set in Connect() - inuse bool +type Socket struct { + protocol int + clientConnected bool + laddr netip.AddrPort // Set in Bind() + raddr netip.AddrPort // Set in Connect() + sock // Device socket, as returned from w.getSocket() } type Config struct { @@ -214,17 +215,13 @@ type wifinina struct { killWatchdog chan bool fault error - sockets map[sock]*socket // keyed by sock as returned by getSocket() -} - -func newSocket(protocol int) *socket { - return &socket{protocol: protocol, inuse: true} + sockets map[int]*Socket // keyed by sockfd } func New(cfg *Config) *wifinina { w := wifinina{ cfg: cfg, - sockets: make(map[sock]*socket), + sockets: make(map[int]*Socket), killWatchdog: make(chan bool), cs: cfg.Cs, ack: cfg.Ack, @@ -505,6 +502,12 @@ func (w *wifinina) GetHostByName(name string) (netip.Addr, error) { fmt.Printf("[GetHostByName] name: %s\r\n", name) } + // If it's already in dotted-decimal notation, return a copy + // per gethostbyname(3). + if ip, err := netip.ParseAddr(name); err == nil { + return ip, nil + } + w.mu.Lock() defer w.mu.Unlock() @@ -547,6 +550,20 @@ func (w *wifinina) Addr() (netip.Addr, error) { return ip, nil } +// newSockfd returns the next available sockfd, or -1 if none available +func (w *wifinina) newSockfd() int { + if len(w.sockets) >= maxNetworks { + return -1 + } + // Search for the next available sockfd starting at 0 + for sockfd := 0; ; sockfd++ { + if _, ok := w.sockets[sockfd]; !ok { + return sockfd + } + } + return -1 +} + // See man socket(2) for standard Berkely sockets for Socket, Bind, etc. // The driver strives to meet the function and semantics of socket(2). @@ -574,34 +591,46 @@ func (w *wifinina) Socket(domain int, stype int, protocol int) (int, error) { w.mu.Lock() defer w.mu.Unlock() - sock := w.getSocket() - if sock == noSocketAvail { + sockfd := w.newSockfd() + if sockfd == -1 { return -1, netdev.ErrNoMoreSockets } - socket := newSocket(protocol) - w.sockets[sock] = socket + w.sockets[sockfd] = &Socket{ + protocol: protocol, + sock: noSocketAvail, + } - return int(sock), nil + if debugging(debugNetdev) { + fmt.Printf("[Socket] <-- sockfd %d\r\n", sockfd) + } + + return sockfd, nil } func (w *wifinina) Bind(sockfd int, ip netip.AddrPort) error { if debugging(debugNetdev) { - fmt.Printf("[Bind] sockfd: %d, addr: %s\r\n", sockfd, ip) + fmt.Printf("[Bind] sockfd: %d, addr: %s:%d\r\n", sockfd, ip.Addr(), ip.Port()) } w.mu.Lock() defer w.mu.Unlock() - var sock = sock(sockfd) - var socket = w.sockets[sock] + socket, ok := w.sockets[sockfd] + if !ok { + return netdev.ErrInvalidSocketFd + } switch socket.protocol { case netdev.IPPROTO_TCP: case netdev.IPPROTO_TLS: case netdev.IPPROTO_UDP: - w.startServer(sock, ip.Port(), protoModeUDP) + socket.sock = w.getSocket() + if socket.sock == noSocketAvail { + return netdev.ErrNoMoreSockets + } + w.startServer(socket.sock, ip.Port(), protoModeUDP) } socket.laddr = ip @@ -629,22 +658,40 @@ func (w *wifinina) Connect(sockfd int, host string, ip netip.AddrPort) error { w.mu.Lock() defer w.mu.Unlock() - var sock = sock(sockfd) - var socket = w.sockets[sock] + socket, ok := w.sockets[sockfd] + if !ok { + return netdev.ErrInvalidSocketFd + } // Start the connection switch socket.protocol { + case netdev.IPPROTO_TCP: - w.startClient(sock, "", toUint32(ip.Addr().As4()), ip.Port(), protoModeTCP) + socket.sock = w.getSocket() + if socket.sock == noSocketAvail { + return netdev.ErrNoMoreSockets + } + w.startClient(socket.sock, "", toUint32(ip.Addr().As4()), ip.Port(), protoModeTCP) + case netdev.IPPROTO_TLS: - w.startClient(sock, host, 0, ip.Port(), protoModeTLS) + socket.sock = w.getSocket() + if socket.sock == noSocketAvail { + return netdev.ErrNoMoreSockets + } + w.startClient(socket.sock, host, 0, ip.Port(), protoModeTLS) + case netdev.IPPROTO_UDP: + if socket.sock == noSocketAvail { + return fmt.Errorf("Must Bind before Connecting") + } // See start in sendUDP() socket.raddr = ip + socket.clientConnected = true return nil } - if w.getClientState(sock) == tcpStateEstablished { + if w.getClientState(socket.sock) == tcpStateEstablished { + socket.clientConnected = true return nil } @@ -664,12 +711,18 @@ func (w *wifinina) Listen(sockfd int, backlog int) error { w.mu.Lock() defer w.mu.Unlock() - var sock = sock(sockfd) - var socket = w.sockets[sock] + socket, ok := w.sockets[sockfd] + if !ok { + return netdev.ErrInvalidSocketFd + } switch socket.protocol { case netdev.IPPROTO_TCP: - w.startServer(sock, socket.laddr.Port(), protoModeTCP) + socket.sock = w.getSocket() + if socket.sock == noSocketAvail { + return netdev.ErrNoMoreSockets + } + w.startServer(socket.sock, socket.laddr.Port(), protoModeTCP) case netdev.IPPROTO_UDP: default: return netdev.ErrProtocolNotSupported @@ -687,9 +740,10 @@ func (w *wifinina) Accept(sockfd int) (int, netip.AddrPort, error) { w.mu.Lock() defer w.mu.Unlock() - var client sock - var sock = sock(sockfd) - var socket = w.sockets[sock] + socket, ok := w.sockets[sockfd] + if !ok { + return -1, netip.AddrPort{}, netdev.ErrInvalidSocketFd + } switch socket.protocol { case netdev.IPPROTO_TCP: @@ -697,8 +751,9 @@ func (w *wifinina) Accept(sockfd int) (int, netip.AddrPort, error) { return -1, netip.AddrPort{}, netdev.ErrProtocolNotSupported } +skip: for { - // Accept() will be sleeping most of the time, checking for a + // Accept() will be sleeping most of the time, checking for // new clients every 1/10 sec. w.mu.Unlock() time.Sleep(100 * time.Millisecond) @@ -709,48 +764,43 @@ func (w *wifinina) Accept(sockfd int) (int, netip.AddrPort, error) { return -1, netip.AddrPort{}, w.fault } - // TODO: BUG: Currently, a sock that is 100% busy will always be - // TODO: returned by w.accept(sock), starving other socks - // TODO: from begin serviced. Need to figure out how to - // TODO: service socks fairly (round-robin?) so no one sock - // TODO: can dominate. - // Check if a client has data - client = w.accept(sock) + var client sock = w.accept(socket.sock) if client == noSocketAvail { // None ready continue } - raddr := w.getRemoteData(client) - - // If we've already seen this socket, we can reuse - // the socket and return it. But, only if the socket - // is closed. If it's not closed, we'll just come back - // later to reuse it. - - clientSocket, ok := w.sockets[client] - if ok { - // Wait for client to Close - if clientSocket.inuse { - continue + // If we already have a socket for the client, skip + for _, s := range w.sockets { + if s.sock == client { + continue skip } - // Reuse client socket - return int(client), raddr, nil } - // Create new socket for client and return fd - w.sockets[client] = newSocket(socket.protocol) - return int(client), raddr, nil + // Otherwise, create a new socket + clientfd := w.newSockfd() + if clientfd == -1 { + return -1, netip.AddrPort{}, netdev.ErrNoMoreSockets + } + + w.sockets[clientfd] = &Socket{ + protocol: netdev.IPPROTO_TCP, + sock: client, + clientConnected: true, + } + + raddr := w.getRemoteData(client) + + return clientfd, raddr, nil } } -func (w *wifinina) sockDown(sock sock) bool { - var socket = w.sockets[sock] +func (w *wifinina) sockDown(socket *Socket) bool { if socket.protocol == netdev.IPPROTO_UDP { return false } - return w.getClientState(sock) != tcpStateEstablished + return w.getClientState(socket.sock) != tcpStateEstablished } func (w *wifinina) sendTCP(sock sock, buf []byte, deadline time.Time) (int, error) { @@ -778,7 +828,7 @@ func (w *wifinina) sendTCP(sock sock, buf []byte, deadline time.Time) (int, erro } // Check if socket went down - if w.sockDown(sock) { + if w.getClientState(sock) != tcpStateEstablished { return -1, io.EOF } @@ -817,8 +867,10 @@ func (w *wifinina) sendUDP(sock sock, raddr netip.AddrPort, buf []byte, deadline } func (w *wifinina) sendChunk(sockfd int, buf []byte, deadline time.Time) (int, error) { - var sock = sock(sockfd) - var socket = w.sockets[sock] + socket, ok := w.sockets[sockfd] + if !ok { + return -1, netdev.ErrInvalidSocketFd + } // Check if we've timed out if !deadline.IsZero() { @@ -829,9 +881,9 @@ func (w *wifinina) sendChunk(sockfd int, buf []byte, deadline time.Time) (int, e switch socket.protocol { case netdev.IPPROTO_TCP, netdev.IPPROTO_TLS: - return w.sendTCP(sock, buf, deadline) + return w.sendTCP(socket.sock, buf, deadline) case netdev.IPPROTO_UDP: - return w.sendUDP(sock, socket.raddr, buf, deadline) + return w.sendUDP(socket.sock, socket.raddr, buf, deadline) } return -1, netdev.ErrProtocolNotSupported @@ -876,7 +928,10 @@ func (w *wifinina) Recv(sockfd int, buf []byte, flags int, w.mu.Lock() defer w.mu.Unlock() - var sock = sock(sockfd) + socket, ok := w.sockets[sockfd] + if !ok { + return -1, netdev.ErrInvalidSocketFd + } // Limit max read size to chunk large read requests var max = len(buf) @@ -897,22 +952,22 @@ func (w *wifinina) Recv(sockfd int, buf []byte, flags int, // doesn't return unless there is data, even a single byte, or // on error such as timeout or EOF. - n := int(w.getDataBuf(sock, buf[:max])) + n := int(w.getDataBuf(socket.sock, buf[:max])) if n > 0 { if debugging(debugNetdev) { fmt.Printf("[<--Recv] sockfd: %d, n: %d\r\n", - sock, n) + sockfd, n) } return n, nil } // Check if socket went down - if w.sockDown(sock) { + if w.sockDown(socket) { // Get any last bytes - n = int(w.getDataBuf(sock, buf[:max])) + n = int(w.getDataBuf(socket.sock, buf[:max])) if debugging(debugNetdev) { fmt.Printf("[<--Recv] sockfd: %d, n: %d, EOF\r\n", - sock, n) + sockfd, n) } if n > 0 { return n, io.EOF @@ -941,34 +996,18 @@ func (w *wifinina) Close(sockfd int) error { w.mu.Lock() defer w.mu.Unlock() - var sock = sock(sockfd) - var socket = w.sockets[sock] - - if !socket.inuse { - return nil + socket, ok := w.sockets[sockfd] + if !ok { + return netdev.ErrInvalidSocketFd } - w.stopClient(sock) - - if socket.protocol == netdev.IPPROTO_UDP { - socket.inuse = false - return nil + if socket.clientConnected { + w.stopClient(socket.sock) } - start := time.Now() - for time.Since(start) < 5*time.Second { + delete(w.sockets, sockfd) - if w.getClientState(sock) == tcpStateClosed { - socket.inuse = false - return nil - } - - w.mu.Unlock() - time.Sleep(100 * time.Millisecond) - w.mu.Lock() - } - - return netdev.ErrClosingSocket + return nil } func (w *wifinina) SetSockOpt(sockfd int, level int, opt int, value interface{}) error { From aa7bd47c684fec42b126ca94e50002be71b263e8 Mon Sep 17 00:00:00 2001 From: Scott Feldman Date: Mon, 22 Jan 2024 13:29:42 -0800 Subject: [PATCH 003/137] examples: add classic network snake test example Adding this example to test multiple concurrent socket connections. --- examples/net/snake/init.go | 30 ++++++++ examples/net/snake/main.go | 120 ++++++++++++++++++++++++++++++ examples/net/snake/server/main.go | 34 +++++++++ 3 files changed, 184 insertions(+) create mode 100644 examples/net/snake/init.go create mode 100644 examples/net/snake/main.go create mode 100644 examples/net/snake/server/main.go diff --git a/examples/net/snake/init.go b/examples/net/snake/init.go new file mode 100644 index 0000000..a00a814 --- /dev/null +++ b/examples/net/snake/init.go @@ -0,0 +1,30 @@ +//go:build ninafw || wioterminal + +package main + +import ( + "log" + "time" + + "tinygo.org/x/drivers/netlink" + "tinygo.org/x/drivers/netlink/probe" +) + +var ( + ssid string + pass string +) + +func init() { + time.Sleep(2 * time.Second) + + link, _ := probe.Probe() + + err := link.NetConnect(&netlink.ConnectParams{ + Ssid: ssid, + Passphrase: pass, + }) + if err != nil { + log.Fatal(err) + } +} diff --git a/examples/net/snake/main.go b/examples/net/snake/main.go new file mode 100644 index 0000000..40bfc65 --- /dev/null +++ b/examples/net/snake/main.go @@ -0,0 +1,120 @@ +// This example is the classic snake network test. The snake is feed a steady +// diet of pkts and the pkts work themselves thru the snake segments and exit +// the tail. Each snake segment is a TCP socket connection to a server. The +// server echos pkts received back to the snake, and serves each segment on a +// different port. (See server/main.go for server). +// +// snake | server +// | +// head ----->---|-->--+ +// seg a | | +// +---<-|--<--+ +// | | +// +-->--|-->--+ +// seg b | | +// +---<-|--<--+ +// | | +// +-->--|-->--+ +// seg c | | +// +---<-|--<--+ +// | | +// +-->--|-->--+ +// ... | | +// +---<-|--<--+ +// | | +// +-->--|-->--+ +// seg n | | +// tail -------<-|--<--+ +// | + +// The snake segments are linked by channels and each segment is run as a go +// func. This forces segments to connect and run concurrently, which is a good +// test of the underlying driver's ability to handle concurrent connections. + +//go:build ninafw || wioterminal + +package main + +import ( + _ "embed" + "fmt" + "log" + "net" + "strings" + "time" +) + +//go:embed main.go +var code string + +var ( + server string = "10.0.0.100:8080" +) + +func segment(in chan []byte, out chan []byte) { + var buf [512]byte + for { + c, err := net.Dial("tcp", server) + for ; err != nil; c, err = net.Dial("tcp", server) { + println(err.Error()) + time.Sleep(5 * time.Second) + } + for { + select { + case msg := <-in: + _, err := c.Write(msg) + if err != nil { + log.Fatal(err.Error()) + } + time.Sleep(100 * time.Millisecond) + n, err := c.Read(buf[:]) + if err != nil { + log.Fatal(err.Error()) + } + out <- buf[:n] + } + } + } +} + +func feedit(head chan []byte) { + for i := 0; i < 100; i++ { + head <- []byte(fmt.Sprintf("\n---%d---\n", i)) + for _, line := range strings.Split(code, "\n") { + if len(line) == 0 { + line = " " + } + head <- []byte(line) + } + } +} + +var head = make(chan []byte) +var a = make(chan []byte) +var b = make(chan []byte) +var c = make(chan []byte) +var d = make(chan []byte) +var e = make(chan []byte) +var f = make(chan []byte) +var tail = make(chan []byte) + +func main() { + + // The snake + go segment(head, a) + go segment(a, b) + go segment(b, c) + go segment(c, d) + go segment(d, e) + go segment(e, f) + go segment(f, tail) + + go feedit(head) + + for { + select { + case msg := <-tail: + println(string(msg)) + } + } +} diff --git a/examples/net/snake/server/main.go b/examples/net/snake/server/main.go new file mode 100644 index 0000000..e67b3eb --- /dev/null +++ b/examples/net/snake/server/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "io" + "log" + "net" +) + +func main() { + // Listen for connections + l, err := net.Listen("tcp", ":8080") + if err != nil { + log.Fatal(err.Error()) + } + defer l.Close() + println("Listening on port", ":8080") + for { + // Wait for a connection + conn, err := l.Accept() + if err != nil { + log.Fatal(err) + } + println("Accepted connection from", conn.RemoteAddr().String()) + // Service the new connection in a goroutine. + // The loop then returns to accepting, so that + // multiple connections may be served concurrently + go func(c net.Conn) { + // Echo all incoming data + io.Copy(c, c) + // Shut down the connection + c.Close() + }(conn) + } +} From c41f2e472d7071b8d113336a8072f29951c6768f Mon Sep 17 00:00:00 2001 From: deadprogram Date: Thu, 25 Jan 2024 13:11:49 +0100 Subject: [PATCH 004/137] rtl8720dn, wifinina: use drivers package version as the driver version Signed-off-by: deadprogram --- rtl8720dn/rtl8720dn.go | 4 ++-- wifinina/wifinina.go | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/rtl8720dn/rtl8720dn.go b/rtl8720dn/rtl8720dn.go index be60f2a..7e073bf 100644 --- a/rtl8720dn/rtl8720dn.go +++ b/rtl8720dn/rtl8720dn.go @@ -17,6 +17,7 @@ import ( "sync" "time" + "tinygo.org/x/drivers" "tinygo.org/x/drivers/netdev" "tinygo.org/x/drivers/netlink" ) @@ -27,7 +28,6 @@ var _debug debug = debugBasic //var _debug debug = debugBasic | debugNetdev | debugRpc var ( - version = "0.0.1" driverName = "Realtek rtl8720dn Wifi network device driver (rtl8720dn)" ) @@ -142,7 +142,7 @@ func (r *rtl8720dn) showDriver() { if debugging(debugBasic) { fmt.Printf("\r\n") fmt.Printf("%s\r\n\r\n", driverName) - fmt.Printf("Driver version : %s\r\n", version) + fmt.Printf("Driver version : %s\r\n", drivers.Version) } r.driverShown = true } diff --git a/wifinina/wifinina.go b/wifinina/wifinina.go index 4f0eea1..d34c85c 100644 --- a/wifinina/wifinina.go +++ b/wifinina/wifinina.go @@ -33,7 +33,6 @@ var _debug debug = debugBasic //var _debug debug = debugBasic | debugNetdev | debugCmd | debugDetail var ( - version = "0.0.1" driverName = "Tinygo ESP32 Wifi network device driver (WiFiNINA)" ) @@ -311,7 +310,7 @@ func (w *wifinina) showDriver() { if debugging(debugBasic) { fmt.Printf("\r\n") fmt.Printf("%s\r\n\r\n", driverName) - fmt.Printf("Driver version : %s\r\n", version) + fmt.Printf("Driver version : %s\r\n", drivers.Version) } w.driverShown = true } From 308763f5007acbf05ba7dd147082ad962ba006bb Mon Sep 17 00:00:00 2001 From: BCG Date: Fri, 26 Jan 2024 07:49:11 -0500 Subject: [PATCH 005/137] Adding driver for rotary encoder support --- README.md | 2 +- encoders/quadrature_device.go | 34 +++++++++ encoders/quadrature_interrupt.go | 69 +++++++++++++++++++ .../encoders/quadrature-interrupt/main.go | 28 ++++++++ smoketest.sh | 1 + 5 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 encoders/quadrature_device.go create mode 100644 encoders/quadrature_interrupt.go create mode 100644 examples/encoders/quadrature-interrupt/main.go diff --git a/README.md b/README.md index b77dcb4..bd30b16 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PkgGoDev](https://pkg.go.dev/badge/tinygo.org/x/drivers)](https://pkg.go.dev/tinygo.org/x/drivers) [![Build](https://github.com/tinygo-org/drivers/actions/workflows/build.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/drivers/actions/workflows/build.yml) -This package provides a collection of 101 different hardware drivers for devices such as sensors and displays that can be used together with [TinyGo](https://tinygo.org). +This package provides a collection of 102 different hardware drivers for devices such as sensors and displays that can be used together with [TinyGo](https://tinygo.org). For the complete list, please see: https://tinygo.org/docs/reference/devices/ diff --git a/encoders/quadrature_device.go b/encoders/quadrature_device.go new file mode 100644 index 0000000..0482a92 --- /dev/null +++ b/encoders/quadrature_device.go @@ -0,0 +1,34 @@ +package encoders + +type QuadratureDevice struct { + cfg QuadratureConfig + impl quadratureImpl +} + +type QuadratureConfig struct { + Precision int +} + +type quadratureImpl interface { + configure(cfg QuadratureConfig) error + readValue() int + writeValue(int) +} + +func (enc *QuadratureDevice) Configure(cfg QuadratureConfig) error { + if cfg.Precision < 1 { + cfg.Precision = 4 + } + enc.cfg = cfg + return enc.impl.configure(cfg) +} + +// Position returns the stored int value for the encoder +func (enc *QuadratureDevice) Position() int { + return enc.impl.readValue() / enc.cfg.Precision +} + +// SetPosition overwrites the currently stored value with the specified int value +func (enc *QuadratureDevice) SetPosition(v int) { + enc.impl.writeValue(v * enc.cfg.Precision) +} diff --git a/encoders/quadrature_interrupt.go b/encoders/quadrature_interrupt.go new file mode 100644 index 0000000..98a7927 --- /dev/null +++ b/encoders/quadrature_interrupt.go @@ -0,0 +1,69 @@ +//go:build tinygo && (rp2040 || stm32 || k210 || esp32c3 || nrf || (avr && (atmega328p || atmega328pb))) + +// Implementation based on: +// https://gist.github.com/aykevl/3fc1683ed77bb0a9c07559dfe857304a + +// Note: build constraints in this file list targets that define machine.PinToggle. +// If this is supported for additional targets in the future, they can be added above. + +package encoders + +import ( + "machine" + "runtime/volatile" +) + +var ( + states = []int8{0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0} +) + +// NewQuadratureViaInterrupt returns a rotary encoder device that uses GPIO +// interrupts and a lookup table to keep track of quadrature state changes. +// +// This constructur is only available for TinyGo targets for which machine.PinToggle +// is defined as a valid interrupt type. +func NewQuadratureViaInterrupt(pinA, pinB machine.Pin) *QuadratureDevice { + return &QuadratureDevice{impl: &quadInterruptImpl{pinA: pinA, pinB: pinB, oldAB: 0b00000011}} +} + +type quadInterruptImpl struct { + pinA machine.Pin + pinB machine.Pin + + // precision int + + oldAB int + value volatile.Register32 +} + +func (enc *quadInterruptImpl) configure(cfg QuadratureConfig) error { + enc.pinA.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + enc.pinA.SetInterrupt(machine.PinToggle, enc.interrupt) + + enc.pinB.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + enc.pinB.SetInterrupt(machine.PinToggle, enc.interrupt) + + return nil +} + +func (enc *quadInterruptImpl) interrupt(pin machine.Pin) { + aHigh, bHigh := enc.pinA.Get(), enc.pinB.Get() + enc.oldAB <<= 2 + if aHigh { + enc.oldAB |= 1 << 1 + } + if bHigh { + enc.oldAB |= 1 + } + enc.writeValue(enc.readValue() + int(states[enc.oldAB&0x0f])) +} + +// readValue gets the value using volatile operations and returns it as an int +func (enc *quadInterruptImpl) readValue() int { + return int(enc.value.Get()) +} + +// writeValue set the value to the specified int using volatile operations +func (enc *quadInterruptImpl) writeValue(v int) { + enc.value.Set(uint32(v)) +} diff --git a/examples/encoders/quadrature-interrupt/main.go b/examples/encoders/quadrature-interrupt/main.go new file mode 100644 index 0000000..8ded393 --- /dev/null +++ b/examples/encoders/quadrature-interrupt/main.go @@ -0,0 +1,28 @@ +//go:build macropad_rp2040 + +package main + +import ( + "machine" + + "tinygo.org/x/drivers/encoders" +) + +var ( + enc = encoders.NewQuadratureViaInterrupt(machine.ROT_A, machine.ROT_B) +) + +func main() { + + enc.Configure(encoders.QuadratureConfig{ + Precision: 4, + }) + + for oldValue := 0; ; { + if newValue := enc.Position(); newValue != oldValue { + println("value: ", newValue) + oldValue = newValue + } + } + +} diff --git a/smoketest.sh b/smoketest.sh index ae45239..b7530b2 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -129,6 +129,7 @@ tinygo build -size short -o ./build/test.hex -target=microbit ./examples/ndir/ma tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/ndir/main_ndir.go tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mpu9150/main.go tinygo build -size short -o ./build/test.hex -target=macropad-rp2040 ./examples/sh1106/macropad_spi +tinygo build -size short -o ./build/test.hex -target=macropad-rp2040 ./examples/encoders/quadrature-interrupt # network examples (espat) tinygo build -size short -o ./build/test.hex -target=challenger-rp2040 ./examples/net/ntpclient/ # network examples (wifinina) From c344e5d879b7126193d6bfc5b738b7ce1d6c4687 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 21 Feb 2024 01:02:23 +0100 Subject: [PATCH 006/137] ssd1306: improvements needed for Thumby SPI display Signed-off-by: deadprogram --- examples/ssd1306/spi_thumby/main.go | 50 +++++++++++++++++++++++++++++ ssd1306/ssd1306.go | 30 ++++++++++++++--- 2 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 examples/ssd1306/spi_thumby/main.go diff --git a/examples/ssd1306/spi_thumby/main.go b/examples/ssd1306/spi_thumby/main.go new file mode 100644 index 0000000..b2a41e5 --- /dev/null +++ b/examples/ssd1306/spi_thumby/main.go @@ -0,0 +1,50 @@ +// This example using the SSD1306 OLED display over SPI on the Thumby board +// A very tiny 72x40 display. +package main + +import ( + "image/color" + "machine" + "time" + + "tinygo.org/x/drivers/ssd1306" +) + +func main() { + machine.SPI0.Configure(machine.SPIConfig{}) + display := ssd1306.NewSPI(machine.SPI0, machine.THUMBY_DC_PIN, machine.THUMBY_RESET_PIN, machine.THUMBY_CS_PIN) + display.Configure(ssd1306.Config{ + Width: 72, + Height: 40, + ResetCol: ssd1306.ResetValue{28, 99}, + ResetPage: ssd1306.ResetValue{0, 5}, + }) + + display.ClearDisplay() + + x := int16(36) + y := int16(20) + deltaX := int16(1) + deltaY := int16(1) + for { + pixel := display.GetPixel(x, y) + c := color.RGBA{255, 255, 255, 255} + if pixel { + c = color.RGBA{0, 0, 0, 255} + } + display.SetPixel(x, y, c) + display.Display() + + x += deltaX + y += deltaY + + if x == 0 || x == 71 { + deltaX = -deltaX + } + + if y == 0 || y == 39 { + deltaY = -deltaY + } + time.Sleep(1 * time.Millisecond) + } +} diff --git a/ssd1306/ssd1306.go b/ssd1306/ssd1306.go index 2f27096..53b38ba 100644 --- a/ssd1306/ssd1306.go +++ b/ssd1306/ssd1306.go @@ -13,6 +13,8 @@ import ( "tinygo.org/x/drivers/internal/legacy" ) +type ResetValue [2]byte + // Device wraps I2C or SPI connection. type Device struct { bus Buser @@ -22,6 +24,8 @@ type Device struct { bufferSize int16 vccState VccMode canReset bool + resetCol ResetValue + resetPage ResetValue } // Config is the configuration for the display @@ -30,6 +34,13 @@ type Config struct { Height int16 VccState VccMode Address uint16 + // ResetCol and ResetPage are used to reset the screen to 0x0 + // This is useful for some screens that have a different size than 128x64 + // For example, the Thumby's screen is 72x40 + // The default values are normally set automatically based on the size. + // If you're using a different size, you might need to set these values manually. + ResetCol ResetValue + ResetPage ResetValue } type I2CBus struct { @@ -79,6 +90,7 @@ func NewSPI(bus drivers.SPI, dcPin, resetPin, csPin machine.Pin) Device { // Configure initializes the display with default configuration func (d *Device) Configure(cfg Config) { + var zeroReset ResetValue if cfg.Width != 0 { d.width = cfg.Width } else { @@ -97,6 +109,16 @@ func (d *Device) Configure(cfg Config) { } else { d.vccState = SWITCHCAPVCC } + if cfg.ResetCol != zeroReset { + d.resetCol = cfg.ResetCol + } else { + d.resetCol = ResetValue{0, uint8(d.width - 1)} + } + if cfg.ResetPage != zeroReset { + d.resetPage = cfg.ResetPage + } else { + d.resetPage = ResetValue{0, uint8(d.height/8) - 1} + } d.bufferSize = d.width * d.height / 8 d.buffer = make([]byte, d.bufferSize) d.canReset = cfg.Address != 0 || d.width != 128 || d.height != 64 // I2C or not 128x64 @@ -186,11 +208,11 @@ func (d *Device) Display() error { // Since we're printing the whole buffer, avoid resetting it in this case if d.canReset { d.Command(COLUMNADDR) - d.Command(0) - d.Command(uint8(d.width - 1)) + d.Command(d.resetCol[0]) + d.Command(d.resetCol[1]) d.Command(PAGEADDR) - d.Command(0) - d.Command(uint8(d.height/8) - 1) + d.Command(d.resetPage[0]) + d.Command(d.resetPage[1]) } return d.Tx(d.buffer, false) From 8665d44e0f63bce1b99b7dd1212b8de06eb7fc05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Maggioni?= Date: Sat, 24 Feb 2024 19:16:06 +0100 Subject: [PATCH 007/137] Use int16 for ADXL345 readings (#656) ADXL345: fix: use int16 for ADXL345 readings --- adxl345/adxl345.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/adxl345/adxl345.go b/adxl345/adxl345.go index aa9e625..e6fbc38 100644 --- a/adxl345/adxl345.go +++ b/adxl345/adxl345.go @@ -95,16 +95,16 @@ func (d *Device) Restart() { func (d *Device) ReadAcceleration() (x int32, y int32, z int32, err error) { rx, ry, rz := d.ReadRawAcceleration() - x = d.dataFormat.convertToIS(rx) - y = d.dataFormat.convertToIS(ry) - z = d.dataFormat.convertToIS(rz) + x = int32(d.dataFormat.convertToIS(rx)) + y = int32(d.dataFormat.convertToIS(ry)) + z = int32(d.dataFormat.convertToIS(rz)) return } // ReadRawAcceleration reads the sensor values and returns the raw x, y and z axis // from the adxl345. -func (d *Device) ReadRawAcceleration() (x int32, y int32, z int32) { +func (d *Device) ReadRawAcceleration() (x int16, y int16, z int16) { data := []byte{0, 0, 0, 0, 0, 0} legacy.ReadRegister(d.bus, uint8(d.Address), REG_DATAX0, data) @@ -140,7 +140,7 @@ func (d *Device) SetRange(sensorRange Range) bool { } // convertToIS adjusts the raw values from the adxl345 with the range configuration -func (d *dataFormat) convertToIS(rawValue int32) int32 { +func (d *dataFormat) convertToIS(rawValue int16) int16 { switch d.sensorRange { case RANGE_2G: return rawValue * 4 // rawValue * 2 * 1000 / 512 @@ -190,6 +190,6 @@ func (b *bwRate) toByte() (bits uint8) { } // readInt converts two bytes to int16 -func readIntLE(msb byte, lsb byte) int32 { - return int32(uint16(msb) | uint16(lsb)<<8) +func readIntLE(msb byte, lsb byte) int16 { + return int16(uint16(msb) | uint16(lsb)<<8) } From 0ea969f25aad8c3c515af9e14b2a8bdfbe069804 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Mon, 26 Feb 2024 19:16:36 +0100 Subject: [PATCH 008/137] docs: update LICENSE year Signed-off-by: deadprogram --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 658115a..40bc82b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018-2023 The TinyGo Authors. All rights reserved. +Copyright (c) 2018-2024 The TinyGo Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are From 31f09d93b832c2e2629cb175e7d866651c383cd8 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Mon, 26 Feb 2024 19:17:49 +0100 Subject: [PATCH 009/137] all: update for relase v0.27 Signed-off-by: deadprogram --- CHANGELOG.md | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++ version.go | 2 +- 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 239e95b..4e713bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,86 @@ +0.27.0 +--- +- **core** + - prepare for CGo changes in TinyGo + +- **new devices** + - **adafruit4650** + - support for Adafruit 4650 feather OLED + - **net** + - new networking support based on tinygo net package + - **pixel** + - add package for efficiently working with raw pixel buffers + - **rotary** + - Adding driver for rotary encoder support + - **seesaw** + - Adding support for Adafruit Seesaw platform + - **sgp30** + - add SGP30 air quality sensor + - **sk6812** + - added support for SK6812 to WS2812 device (#610) + +- **enhancements** + - **epd2in13** + - add Sleep method like other displays + - unify rotation configuration with other displays + - use better black/white approximation + - **ili9341** + - add DrawBitmap method + - **lora/lorawan** + - LoRa WAN US915 Support + - LoRa WAN add setter functions + - refactor shared functionality for channels/regions + - **mcp2515** + - Add more line speeds to mcp2515.go (#626) + - **rtl8720dn** + - use drivers package version as the driver version + - **ssd1306** + - improvements needed for Thumby SPI display + - **st7735** + - make the display generic over RGB565 and RGB444 + - **st7789** + - add DrawBitmap method + - make the display generic over RGB565 and RGB444 + - **wifinina** + - add ResetIsHigh cfg switch for MKR 1010 (copied from #561) + - maintenence. Also see PR #4085 in the main TinyGo repo + - use drivers package version as the driver version + +- **bugfixes** + - **adxl345** + - Use int16 for ADXL345 readings (#656) + - **at24cx** + - fixed the description of the device struct + - **rtl8720dn** + - allow connecting to open wifi access points + - fix check for bad Wifi connect + - **sh1106** + - fix I2C interface and add smoketest + - fixed the description of the device struct + - **wifinina** + - add 'unknown failure' reason code for AP connect + - fix concurrency issues with multiple sockets + - fix wifinina UDP send + +- **examples** + - **ds3231** + - fix the description in the example + - **lorawan** + - add missing functions for simulated interface + - modify atcmd and basic demo to support choosing any one of the supported regions at compile time by using ldflags + - **net** + - all networking examples now using netdev and netlink. + +- **build** + - **all** + - fix broken testrunner + - migrated legacy I2C + - add natiu package for tests + - **smoketest** + - add stack-size param for net tests. + - allow stack-size flag as it is needed for net examples + + 0.26.0 --- - **core** diff --git a/version.go b/version.go index 2f0b54b..a754383 100644 --- a/version.go +++ b/version.go @@ -2,4 +2,4 @@ package drivers // Version returns a user-readable string showing the version of the drivers package for support purposes. // Update this value before release of new version of software. -const Version = "0.26.0" +const Version = "0.27.0" From 22581dfd5841296f2f0e444809b4c100af973401 Mon Sep 17 00:00:00 2001 From: sago35 Date: Thu, 29 Feb 2024 20:26:37 +0900 Subject: [PATCH 010/137] encoders: add atsamd21, atsamd51, atsame5x --- encoders/quadrature_interrupt.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/encoders/quadrature_interrupt.go b/encoders/quadrature_interrupt.go index 98a7927..67c70bc 100644 --- a/encoders/quadrature_interrupt.go +++ b/encoders/quadrature_interrupt.go @@ -1,4 +1,4 @@ -//go:build tinygo && (rp2040 || stm32 || k210 || esp32c3 || nrf || (avr && (atmega328p || atmega328pb))) +//go:build tinygo && (rp2040 || stm32 || k210 || esp32c3 || nrf || sam || (avr && (atmega328p || atmega328pb))) // Implementation based on: // https://gist.github.com/aykevl/3fc1683ed77bb0a9c07559dfe857304a From 3ed09d3fe1b202e83b001dc6b314327ba1dc47d3 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 31 Mar 2024 09:43:13 +0200 Subject: [PATCH 011/137] wifinina: implement ConnectModeAP Signed-off-by: deadprogram --- wifinina/wifinina.go | 90 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 83 insertions(+), 7 deletions(-) diff --git a/wifinina/wifinina.go b/wifinina/wifinina.go index d34c85c..a70c825 100644 --- a/wifinina/wifinina.go +++ b/wifinina/wifinina.go @@ -47,6 +47,9 @@ const ( statusConnectFailed connectionStatus = 4 statusConnectionLost connectionStatus = 5 statusDisconnected connectionStatus = 6 + statusAPListening connectionStatus = 7 + statusAPConnected connectionStatus = 8 + statusAPFailed connectionStatus = 9 encTypeTKIP encryptionType = 2 encTypeCCMP encryptionType = 4 @@ -299,6 +302,61 @@ func (w *wifinina) connectToAP() error { return netlink.ErrConnectTimeout } +func (w *wifinina) startAP() error { + timeout := w.params.ConnectTimeout + if timeout == 0 { + timeout = netlink.DefaultConnectTimeout + } + + if len(w.params.Ssid) == 0 { + return netlink.ErrMissingSSID + } + + if debugging(debugBasic) { + fmt.Printf("Starting Wifi AP as SSID '%s'...", w.params.Ssid) + } + + start := time.Now() + + // Start the connection process + switch { + case w.params.Passphrase != "": + w.setPassphraseForAP(w.params.Ssid, w.params.Passphrase) + default: + w.setNetworkForAP(w.params.Ssid) + } + + // Check if we are listening + for { + status := w.getConnectionStatus() + switch status { + case statusAPListening: + if debugging(debugBasic) { + fmt.Printf("LISTENING\r\n") + } + if w.notifyCb != nil { + w.notifyCb(netlink.EventNetUp) + } + return nil + case statusAPFailed: + if debugging(debugBasic) { + fmt.Printf("FAILED (%s)\r\n", w.reason()) + } + return netlink.ErrConnectFailed + } + if time.Since(start) > timeout { + break + } + time.Sleep(1 * time.Second) + } + + if debugging(debugBasic) { + fmt.Printf("FAILED (timed out)\r\n") + } + + return netlink.ErrConnectTimeout +} + func (w *wifinina) netDisconnect() { w.disconnect() } @@ -380,7 +438,12 @@ func (w *wifinina) showIP() { } func (w *wifinina) networkDown() bool { - return w.getConnectionStatus() != statusConnected + switch w.getConnectionStatus() { + case statusConnected, statusAPListening, statusAPConnected: + return false + default: + return true + } } func (w *wifinina) watchdog() { @@ -418,15 +481,28 @@ func (w *wifinina) netConnect(reset bool) error { } w.showDevice() +retry: for i := 0; w.params.Retries == 0 || i < w.params.Retries; i++ { - if err := w.connectToAP(); err != nil { - switch err { - case netlink.ErrConnectTimeout, netlink.ErrConnectFailed: - continue + switch w.params.ConnectMode { + case netlink.ConnectModeAP: + if err := w.startAP(); err != nil { + switch err { + case netlink.ErrConnectTimeout, netlink.ErrConnectFailed: + continue + } + return err } - return err + break retry + default: + if err := w.connectToAP(); err != nil { + switch err { + case netlink.ErrConnectTimeout, netlink.ErrConnectFailed: + continue + } + return err + } + break retry } - break } if w.networkDown() { From a74770b2e6fe38056c4f1ff505df12bd9d8044c9 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 31 Mar 2024 10:57:19 +0200 Subject: [PATCH 012/137] rtl8720dn: implement ConnectModeAP Signed-off-by: deadprogram --- netdev/netdev.go | 1 + rtl8720dn/rtl8720dn.go | 73 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 67 insertions(+), 7 deletions(-) diff --git a/netdev/netdev.go b/netdev/netdev.go index 01a8bf8..52cf529 100644 --- a/netdev/netdev.go +++ b/netdev/netdev.go @@ -36,6 +36,7 @@ var ( ErrFamilyNotSupported = errors.New("Address family not supported") ErrProtocolNotSupported = errors.New("Socket protocol/type not supported") ErrStartingDHCPClient = errors.New("Error starting DHPC client") + ErrStartingDHCPServer = errors.New("Error starting DHPC server") ErrNoMoreSockets = errors.New("No more sockets") ErrClosingSocket = errors.New("Error closing socket") ErrNotSupported = errors.New("Not supported") diff --git a/rtl8720dn/rtl8720dn.go b/rtl8720dn/rtl8720dn.go index 7e073bf..6493f06 100644 --- a/rtl8720dn/rtl8720dn.go +++ b/rtl8720dn/rtl8720dn.go @@ -32,8 +32,9 @@ var ( ) const ( - O_NONBLOCK = 1 // note: different value than syscall.O_NONBLOCK (0x800) - RTW_MODE_STA = 0x00000001 + O_NONBLOCK = 1 // note: different value than syscall.O_NONBLOCK (0x800) + RTW_MODE_STA = 0x00000001 + defaultChannel = 6 ) type sock int32 @@ -135,6 +136,51 @@ func (r *rtl8720dn) connectToAP() error { return r.startDhcpc() } +func (r *rtl8720dn) startDhcps() error { + if result := r.rpc_tcpip_adapter_dhcps_start(0); result == -1 { + return netdev.ErrStartingDHCPServer + } + return nil +} + +func (r *rtl8720dn) startAP() error { + if len(r.params.Ssid) == 0 { + return netlink.ErrMissingSSID + } + + if len(r.params.Passphrase) != 0 && len(r.params.Passphrase) < 8 { + return netlink.ErrShortPassphrase + } + + if debugging(debugBasic) { + fmt.Printf("Starting Wifi AP as SSID '%s'...", r.params.Ssid) + } + + // Start the connection process + securityType := uint32(0) // RTW_SECURITY_OPEN + if len(r.params.Passphrase) != 0 { + securityType = 0x00400004 // RTW_SECURITY_WPA2_AES_PSK + } + + result := r.rpc_wifi_start_ap(r.params.Ssid, r.params.Passphrase, securityType, defaultChannel) + if result != 0 { + if debugging(debugBasic) { + fmt.Printf("FAILED\r\n") + } + return netlink.ErrConnectFailed + } + + if debugging(debugBasic) { + fmt.Printf("LISTENING\r\n") + } + + if r.notifyCb != nil { + r.notifyCb(netlink.EventNetUp) + } + + return r.startDhcps() +} + func (r *rtl8720dn) showDriver() { if r.driverShown { return @@ -246,14 +292,27 @@ func (r *rtl8720dn) netConnect(reset bool) error { } r.showDevice() +retry: for i := 0; r.params.Retries == 0 || i < r.params.Retries; i++ { - if err := r.connectToAP(); err != nil { - if err == netlink.ErrConnectFailed { - continue + switch r.params.ConnectMode { + case netlink.ConnectModeAP: + if err := r.startAP(); err != nil { + if err == netlink.ErrConnectFailed { + continue + } + return err } - return err + break retry + + default: + if err := r.connectToAP(); err != nil { + if err == netlink.ErrConnectFailed { + continue + } + return err + } + break retry } - break } if r.networkDown() { From 60801ba8527d6f2eff925e03f6eb49657552b7bc Mon Sep 17 00:00:00 2001 From: deadprogram Date: Fri, 1 Mar 2024 21:31:06 +0100 Subject: [PATCH 013/137] pixel: add support for Monochrome types such as the SSD1306 display Signed-off-by: deadprogram --- pixel/image.go | 57 ++++++++++++++++++++++++++++++++++++++------- pixel/image_test.go | 32 +++++++++++++++++++++++++ pixel/pixel.go | 35 ++++++++++++++++++++++++++-- 3 files changed, 114 insertions(+), 10 deletions(-) diff --git a/pixel/image.go b/pixel/image.go index 54633e3..0a3f727 100644 --- a/pixel/image.go +++ b/pixel/image.go @@ -22,12 +22,13 @@ func NewImage[T Color](width, height int) Image[T] { } var zeroColor T var data unsafe.Pointer - if zeroColor.BitsPerPixel()%8 == 0 { + switch { + case zeroColor.BitsPerPixel()%8 == 0: // Typical formats like RGB888 and RGB565. // Each color starts at a whole byte offset from the start. buf := make([]T, width*height) data = unsafe.Pointer(&buf[0]) - } else { + default: // Formats like RGB444 that have 12 bits per pixel. // We access these as bytes, so allocate the buffer as a byte slice. bufBits := width * height * zeroColor.BitsPerPixel() @@ -80,10 +81,11 @@ func (img Image[T]) Len() int { func (img Image[T]) RawBuffer() []uint8 { var zeroColor T var numBytes int - if zeroColor.BitsPerPixel()%8 == 0 { + switch { + case zeroColor.BitsPerPixel()%8 == 0: // Each color starts at a whole byte offset. numBytes = int(unsafe.Sizeof(zeroColor)) * int(img.width) * int(img.height) - } else { + default: // Formats like RGB444 that aren't a whole number of bytes. numBits := zeroColor.BitsPerPixel() * int(img.width) * int(img.height) numBytes = (numBits + 7) / 8 // round up (see NewImage) @@ -99,7 +101,20 @@ func (img Image[T]) Size() (int, int) { func (img Image[T]) setPixel(index int, c T) { var zeroColor T - if zeroColor.BitsPerPixel()%8 == 0 { + switch { + case zeroColor.BitsPerPixel() == 1: + // Monochrome. + x := int16(index) % img.width + y := int16(index) / img.width + offset := x + (y/8)*img.width + ptr := (*byte)(unsafe.Add(img.data, offset)) + if c != zeroColor { + *((*byte)(ptr)) |= 1 << uint8(y%8) + } else { + *((*byte)(ptr)) &^= 1 << uint8(y%8) + } + return + case zeroColor.BitsPerPixel()%8 == 0: // Each color starts at a whole byte offset. // This is the easy case. offset := index * int(unsafe.Sizeof(zeroColor)) @@ -147,7 +162,15 @@ func (img Image[T]) Get(x, y int) T { var zeroColor T index := y*int(img.width) + x // index into img.data - if zeroColor.BitsPerPixel()%8 == 0 { + switch { + case zeroColor.BitsPerPixel() == 1: + // Monochrome. + var c Monochrome + offset := x + (y/8)*int(img.width) + ptr := (*byte)(unsafe.Add(img.data, offset)) + c = (*ptr >> uint8(y%8) & 0x1) == 1 + return any(c).(T) + case zeroColor.BitsPerPixel()%8 == 0: // Colors like RGB565, RGB888, etc. offset := index * int(unsafe.Sizeof(zeroColor)) ptr := unsafe.Add(img.data, offset) @@ -181,8 +204,26 @@ func (img Image[T]) Get(x, y int) T { func (img Image[T]) FillSolidColor(color T) { var zeroColor T - // Fast pass for colors of 8, 16, 24, etc bytes in size. - if zeroColor.BitsPerPixel()%8 == 0 { + switch { + case zeroColor.BitsPerPixel() == 1: + // Monochrome. + var colorByte uint8 + if color != zeroColor { + colorByte = 0xff + } + numBytes := int(img.width) * int(img.height) / 8 + for i := 0; i < numBytes; i++ { + // TODO: this can be optimized a lot. + // - The store can be done as a 32-bit integer, after checking for + // alignment. + // - Perhaps the loop can be unrolled to improve copy performance. + ptr := (*byte)(unsafe.Add(img.data, i)) + *((*byte)(ptr)) = colorByte + } + return + + case zeroColor.BitsPerPixel()%8 == 0: + // Fast pass for colors of 8, 16, 24, etc bytes in size. ptr := img.data for i := 0; i < img.Len(); i++ { // TODO: this can be optimized a lot. diff --git a/pixel/image_test.go b/pixel/image_test.go index 42f292c..e7f165e 100644 --- a/pixel/image_test.go +++ b/pixel/image_test.go @@ -62,3 +62,35 @@ func TestImageRGB444BE(t *testing.T) { } } } + +func TestImageMonochrome(t *testing.T) { + image := pixel.NewImage[pixel.Monochrome](5, 3) + if width, height := image.Size(); width != 5 && height != 3 { + t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height) + } + for _, expected := range []color.RGBA{ + {R: 0xff, G: 0xff, B: 0xff}, + {G: 0xff}, + {R: 0xff, G: 0xff}, + {G: 0xff, B: 0xff}, + {R: 0x00}, + {G: 0x00, A: 0xff}, + {B: 0x00, A: 0xff}, + } { + encoded := pixel.NewColor[pixel.Monochrome](expected.R, expected.G, expected.B) + image.Set(4, 2, encoded) + actual := image.Get(4, 2).RGBA() + switch { + case expected.R == 0 && expected.G == 0 && expected.B == 0: + // should be false eg black + if actual.R != 0 || actual.G != 0 || actual.B != 0 { + t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual) + } + case int(expected.R)+int(expected.G)+int(expected.B) > 128*3: + // should be true eg white + if actual.R == 0 || actual.G == 0 || actual.B == 0 { + t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual) + } + } + } +} diff --git a/pixel/pixel.go b/pixel/pixel.go index 17db4f3..ed91a60 100644 --- a/pixel/pixel.go +++ b/pixel/pixel.go @@ -14,9 +14,9 @@ import ( // Pixel with a particular color, matching the underlying hardware of a // particular display. Each pixel is at least 1 byte in size. -// The color format is sRGB (or close to it) in all cases. +// The color format is sRGB (or close to it) in all cases except for 1-bit. type Color interface { - RGB888 | RGB565BE | RGB555 | RGB444BE + RGB888 | RGB565BE | RGB555 | RGB444BE | Monochrome BaseColor } @@ -50,6 +50,8 @@ func NewColor[T Color](r, g, b uint8) T { return any(NewRGB555(r, g, b)).(T) case RGB444BE: return any(NewRGB444BE(r, g, b)).(T) + case Monochrome: + return any(NewMonochrome(r, g, b)).(T) default: panic("unknown color format") } @@ -202,6 +204,35 @@ func (c RGB444BE) RGBA() color.RGBA { return color } +type Monochrome bool + +func NewMonochrome(r, g, b uint8) Monochrome { + // Very simple black/white split. + // This isn't very accurate (especially for sRGB colors) but is close enough. + if int(r)+int(g)+int(b) > 128*3 { // light, convert to white + return Monochrome(true) + } + // dark, convert to black + return Monochrome(false) +} + +func (c Monochrome) BitsPerPixel() int { + return 1 +} + +func (c Monochrome) RGBA() color.RGBA { + value := uint8(0) + if c { + value = 255 + } + return color.RGBA{ + R: value, + G: value, + B: value, + A: 255, + } +} + // Gamma brightness lookup table: // https://victornpb.github.io/gamma-table-generator // gamma = 0.45 steps = 256 range = 0-255 From a0293643b318c5e28d8429f0fe1dab02ec521d11 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Mon, 15 Apr 2024 22:12:14 +0200 Subject: [PATCH 014/137] ssd1306: add DrawBitmap() function to complete Displayer interface Signed-off-by: deadprogram --- ssd1306/ssd1306.go | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/ssd1306/ssd1306.go b/ssd1306/ssd1306.go index 53b38ba..fdf168f 100644 --- a/ssd1306/ssd1306.go +++ b/ssd1306/ssd1306.go @@ -11,6 +11,12 @@ import ( "tinygo.org/x/drivers" "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/pixel" +) + +var ( + errBufferSize = errors.New("invalid size buffer") + errOutOfRange = errors.New("out of screen range") ) type ResetValue [2]byte @@ -245,8 +251,7 @@ func (d *Device) GetPixel(x int16, y int16) bool { // SetBuffer changes the whole buffer at once func (d *Device) SetBuffer(buffer []byte) error { if int16(len(buffer)) != d.bufferSize { - //return ErrBuffer - return errors.New("wrong size buffer") + return errBufferSize } for i := int16(0); i < d.bufferSize; i++ { d.buffer[i] = buffer[i] @@ -338,3 +343,19 @@ func (b *SPIBus) tx(data []byte, isCommand bool) error { func (d *Device) Size() (w, h int16) { return d.width, d.height } + +// DrawBitmap copies the bitmap to the screen at the given coordinates. +func (d *Device) DrawBitmap(x, y int16, bitmap pixel.Image[pixel.Monochrome]) error { + width, height := bitmap.Size() + if x < 0 || x+int16(width) > d.width || y < 0 || y+int16(height) > d.height { + return errOutOfRange + } + + for i := 0; i < width; i++ { + for j := 0; j < height; j++ { + d.SetPixel(x+int16(i), y+int16(j), bitmap.Get(i, j).RGBA()) + } + } + + return nil +} From f206f6f56f3e168e7a642811c3d223930b5fa5af Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Fri, 19 Apr 2024 13:10:46 +0200 Subject: [PATCH 015/137] ft6336: ignore bogus touch events At least one ft6336 device reports all 255 values from the first read after reset or poweron, even after waiting the specified 300ms reset delay. Signed-off-by: Elias Naur --- ft6336/ft6336.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ft6336/ft6336.go b/ft6336/ft6336.go index e6eb2c6..6b2032d 100644 --- a/ft6336/ft6336.go +++ b/ft6336/ft6336.go @@ -80,7 +80,8 @@ func (d *Device) Read() []byte { func (d *Device) ReadTouchPoint() touch.Point { d.Read() z := 0xFFFFF - if d.buf[0] == 0 { + switch d.buf[0] { + case 0, 255: z = 0 } From bf0b383176a48e9c3d9d3694cfb902fcf3c77897 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 21 Apr 2024 12:19:58 +0200 Subject: [PATCH 016/137] ssd1306: add rotation functions for Displayer interface Signed-off-by: deadprogram --- ssd1306/ssd1306.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ssd1306/ssd1306.go b/ssd1306/ssd1306.go index fdf168f..955a364 100644 --- a/ssd1306/ssd1306.go +++ b/ssd1306/ssd1306.go @@ -15,8 +15,9 @@ import ( ) var ( - errBufferSize = errors.New("invalid size buffer") - errOutOfRange = errors.New("out of screen range") + errBufferSize = errors.New("invalid size buffer") + errOutOfRange = errors.New("out of screen range") + errNotImplemented = errors.New("not implemented") ) type ResetValue [2]byte @@ -359,3 +360,14 @@ func (d *Device) DrawBitmap(x, y int16, bitmap pixel.Image[pixel.Monochrome]) er return nil } + +// Rotation returns the currently configured rotation. +func (d *Device) Rotation() drivers.Rotation { + return drivers.Rotation0 +} + +// SetRotation changes the rotation of the device (clock-wise). +// Would have to be implemented in software for this device. +func (d *Device) SetRotation(rotation drivers.Rotation) error { + return errNotImplemented +} From 1095629f62a3abf52c01e25e347c70e79e1d9e6d Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 21 Apr 2024 12:26:38 +0200 Subject: [PATCH 017/137] ssd1306: add Sleep() function for Displayer interface Signed-off-by: deadprogram --- ssd1306/ssd1306.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ssd1306/ssd1306.go b/ssd1306/ssd1306.go index 955a364..7aaef87 100644 --- a/ssd1306/ssd1306.go +++ b/ssd1306/ssd1306.go @@ -371,3 +371,15 @@ func (d *Device) Rotation() drivers.Rotation { func (d *Device) SetRotation(rotation drivers.Rotation) error { return errNotImplemented } + +// Set the sleep mode for this display. When sleeping, the panel uses a lot +// less power. The display won't show an image anymore, but the memory contents +// should be kept. +func (d *Device) Sleep(sleepEnabled bool) error { + if sleepEnabled { + d.Command(DISPLAYOFF) + } else { + d.Command(DISPLAYON) + } + return nil +} From 5eeba138a48b27accf118270c18b21a7632a92c0 Mon Sep 17 00:00:00 2001 From: sago35 Date: Fri, 3 May 2024 10:31:39 +0900 Subject: [PATCH 018/137] Simplify examples/ws2812 --- examples/ws2812/arduino.go | 3 +-- examples/ws2812/digispark.go | 1 - examples/ws2812/main.go | 8 ++++---- examples/ws2812/others.go | 5 ++--- examples/ws2812/others_wo_led.go | 10 ---------- examples/ws2812/thingplus-rp2040.go | 12 ------------ 6 files changed, 7 insertions(+), 32 deletions(-) delete mode 100644 examples/ws2812/others_wo_led.go delete mode 100644 examples/ws2812/thingplus-rp2040.go diff --git a/examples/ws2812/arduino.go b/examples/ws2812/arduino.go index 3f8c58e..a47b104 100644 --- a/examples/ws2812/arduino.go +++ b/examples/ws2812/arduino.go @@ -4,7 +4,6 @@ package main import "machine" -// Replace neo and led in the code below to match the pin +// Replace neo in the code below to match the pin // that you are using if different. var neo = machine.D2 -var led = machine.LED diff --git a/examples/ws2812/digispark.go b/examples/ws2812/digispark.go index fa8c574..ad7f371 100644 --- a/examples/ws2812/digispark.go +++ b/examples/ws2812/digispark.go @@ -8,4 +8,3 @@ import "machine" // Replace neo and led in the code below to match the pin // that you are using if different. var neo machine.Pin = 0 -var led = machine.LED diff --git a/examples/ws2812/main.go b/examples/ws2812/main.go index 02699d6..60e1962 100644 --- a/examples/ws2812/main.go +++ b/examples/ws2812/main.go @@ -12,11 +12,12 @@ import ( "tinygo.org/x/drivers/ws2812" ) -var leds [10]color.RGBA +var ( + noe machine.Pin + leds [10]color.RGBA +) func main() { - led.Configure(machine.PinConfig{Mode: machine.PinOutput}) - neo.Configure(machine.PinConfig{Mode: machine.PinOutput}) ws := ws2812.NewWS2812(neo) @@ -35,7 +36,6 @@ func main() { } ws.WriteColors(leds[:]) - led.Set(rg) time.Sleep(100 * time.Millisecond) } } diff --git a/examples/ws2812/others.go b/examples/ws2812/others.go index 3c90f05..11becd8 100644 --- a/examples/ws2812/others.go +++ b/examples/ws2812/others.go @@ -1,10 +1,9 @@ -//go:build !digispark && !arduino && !qtpy && !m5stamp_c3 && !thingplus_rp2040 +//go:build !digispark && !arduino package main import "machine" -// Replace neo and led in the code below to match the pin +// Replace neo in the code below to match the pin // that you are using if different. var neo machine.Pin = machine.WS2812 -var led = machine.LED diff --git a/examples/ws2812/others_wo_led.go b/examples/ws2812/others_wo_led.go deleted file mode 100644 index 988d79a..0000000 --- a/examples/ws2812/others_wo_led.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build qtpy || m5stamp_c3 - -package main - -import "machine" - -// Replace neo and led in the code below to match the pin -// that you are using if different. -var neo machine.Pin = machine.WS2812 -var led = machine.NoPin diff --git a/examples/ws2812/thingplus-rp2040.go b/examples/ws2812/thingplus-rp2040.go deleted file mode 100644 index a194bda..0000000 --- a/examples/ws2812/thingplus-rp2040.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build thingplus_rp2040 - -package main - -import "machine" - -// This is the pin assignment for the internal neopixel of the -// Sparkfun thingplus rp2040. -// Replace neo and led in the code below to match the pin -// that you are using if different. -var neo machine.Pin = machine.GPIO8 -var led = machine.LED From bb0e6b8ba8305aad8742a40902cb7dad225bafb7 Mon Sep 17 00:00:00 2001 From: sago35 Date: Sat, 4 May 2024 23:32:27 +0900 Subject: [PATCH 019/137] Fix typo and move initialization of neo to init() --- examples/ws2812/arduino.go | 8 +++++--- examples/ws2812/digispark.go | 10 ++++++---- examples/ws2812/main.go | 2 +- examples/ws2812/others.go | 8 +++++--- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/examples/ws2812/arduino.go b/examples/ws2812/arduino.go index a47b104..6196aec 100644 --- a/examples/ws2812/arduino.go +++ b/examples/ws2812/arduino.go @@ -4,6 +4,8 @@ package main import "machine" -// Replace neo in the code below to match the pin -// that you are using if different. -var neo = machine.D2 +func init() { + // Replace neo in the code below to match the pin + // that you are using if different. + neo = machine.D2 +} diff --git a/examples/ws2812/digispark.go b/examples/ws2812/digispark.go index ad7f371..e7d7526 100644 --- a/examples/ws2812/digispark.go +++ b/examples/ws2812/digispark.go @@ -4,7 +4,9 @@ package main import "machine" -// This is the pin assignment for the Digispark only. -// Replace neo and led in the code below to match the pin -// that you are using if different. -var neo machine.Pin = 0 +func init() { + // This is the pin assignment for the Digispark only. + // Replace neo and led in the code below to match the pin + // that you are using if different. + neo = machine.Pin(0) +} diff --git a/examples/ws2812/main.go b/examples/ws2812/main.go index 60e1962..8ade21f 100644 --- a/examples/ws2812/main.go +++ b/examples/ws2812/main.go @@ -13,7 +13,7 @@ import ( ) var ( - noe machine.Pin + neo machine.Pin leds [10]color.RGBA ) diff --git a/examples/ws2812/others.go b/examples/ws2812/others.go index 11becd8..27e116e 100644 --- a/examples/ws2812/others.go +++ b/examples/ws2812/others.go @@ -4,6 +4,8 @@ package main import "machine" -// Replace neo in the code below to match the pin -// that you are using if different. -var neo machine.Pin = machine.WS2812 +func init() { + // Replace neo in the code below to match the pin + // that you are using if different. + neo = machine.WS2812 +} From 50b6f18cc25e9bcd79d6f84ae5bbad19d939f0cc Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 7 May 2024 21:36:31 +0200 Subject: [PATCH 020/137] uc8151: update to support all functions needed by tinygl and board package Displayer interface Signed-off-by: deadprogram --- examples/uc8151/main.go | 3 +- uc8151/registers.go | 11 ++++--- uc8151/uc8151.go | 71 +++++++++++++++++++++++++++++++++-------- 3 files changed, 66 insertions(+), 19 deletions(-) diff --git a/examples/uc8151/main.go b/examples/uc8151/main.go index e55acb2..c0e7520 100644 --- a/examples/uc8151/main.go +++ b/examples/uc8151/main.go @@ -4,6 +4,7 @@ import ( "image/color" "machine" + "tinygo.org/x/drivers" "tinygo.org/x/drivers/uc8151" ) @@ -21,7 +22,7 @@ func main() { display = uc8151.New(machine.SPI0, machine.EPD_CS_PIN, machine.EPD_DC_PIN, machine.EPD_RESET_PIN, machine.EPD_BUSY_PIN) display.Configure(uc8151.Config{ - Rotation: uc8151.ROTATION_270, + Rotation: drivers.Rotation270, Speed: uc8151.MEDIUM, Blocking: true, }) diff --git a/uc8151/registers.go b/uc8151/registers.go index 386d2c8..c4170c8 100644 --- a/uc8151/registers.go +++ b/uc8151/registers.go @@ -1,5 +1,7 @@ package uc8151 +import "tinygo.org/x/drivers" + // Registers const ( // Display resolution @@ -141,10 +143,11 @@ const ( HZ_100 = 0b00111010 HZ_200 = 0b00111001 - NO_ROTATION Rotation = 0 - ROTATION_90 Rotation = 1 // 90 degrees clock-wise rotation - ROTATION_180 Rotation = 2 - ROTATION_270 Rotation = 3 + // deprecated constants, just here for backward compatibility. + NO_ROTATION = drivers.Rotation0 + ROTATION_90 = drivers.Rotation90 + ROTATION_180 = drivers.Rotation180 + ROTATION_270 = drivers.Rotation270 DEFAULT Speed = 0 MEDIUM Speed = 1 diff --git a/uc8151/uc8151.go b/uc8151/uc8151.go index 866b321..2d4c03b 100644 --- a/uc8151/uc8151.go +++ b/uc8151/uc8151.go @@ -11,13 +11,18 @@ import ( "time" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/pixel" +) + +var ( + errOutOfRange = errors.New("out of screen range") ) type Config struct { Width int16 Height int16 - Rotation Rotation // Rotation is clock-wise - Speed Speed // Value from DEFAULT, MEDIUM, FAST, TURBO + Rotation drivers.Rotation // Rotation is clock-wise + Speed Speed // Value from DEFAULT, MEDIUM, FAST, TURBO Blocking bool } @@ -31,12 +36,11 @@ type Device struct { height int16 buffer []uint8 bufferLength uint32 - rotation Rotation + rotation drivers.Rotation speed Speed blocking bool } -type Rotation uint8 type Speed uint8 // New returns a new epd2in13x driver. Pass in a fully configured SPI bus. @@ -135,6 +139,11 @@ func (d *Device) PowerOff() { d.SendCommand(POF) } +// PowerOn power on the device +func (d *Device) PowerOn() { + d.SendCommand(PON) +} + // SendCommand sends a command to the display func (d *Device) SendCommand(command uint8) { d.sendDataCommand(true, command) @@ -175,6 +184,22 @@ func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { } } +// DrawBitmap copies the bitmap to the screen at the given coordinates. +func (d *Device) DrawBitmap(x, y int16, bitmap pixel.Image[pixel.Monochrome]) error { + width, height := bitmap.Size() + if x < 0 || x+int16(width) > d.width || y < 0 || y+int16(height) > d.height { + return errOutOfRange + } + + for i := 0; i < width; i++ { + for j := 0; j < height; j++ { + d.SetPixel(x+int16(i), y+int16(j), bitmap.Get(i, j).RGBA()) + } + } + + return nil +} + // Display sends the buffer to the screen. func (d *Device) Display() error { if d.blocking { @@ -208,13 +233,14 @@ func (d *Device) DisplayRect(x int16, y int16, width int16, height int16) error if x < 0 || y < 0 || x >= d.width || y >= d.height || width < 0 || height < 0 { return errors.New("wrong rectangle") } - if d.rotation == ROTATION_90 { + switch d.rotation { + case drivers.Rotation0: width, height = height, width x -= width - } else if d.rotation == ROTATION_180 { + case drivers.Rotation90: x -= width - 1 y -= height - 1 - } else if d.rotation == ROTATION_270 { + case drivers.Rotation180: width, height = height, width y -= height } @@ -287,15 +313,32 @@ func (d *Device) ClearBuffer() { // Size returns the current size of the display. func (d *Device) Size() (w, h int16) { - if d.rotation == ROTATION_90 || d.rotation == ROTATION_270 { + if d.rotation == drivers.Rotation90 || d.rotation == drivers.Rotation270 { return d.height, d.width } return d.width, d.height } +// Rotation returns the currently configured rotation. +func (d *Device) Rotation() drivers.Rotation { + return d.rotation +} + // SetRotation changes the rotation (clock-wise) of the device -func (d *Device) SetRotation(rotation Rotation) { +func (d *Device) SetRotation(rotation drivers.Rotation) error { d.rotation = rotation + return nil +} + +// Set the sleep mode for this display. +func (d *Device) Sleep(sleepEnabled bool) error { + if sleepEnabled { + d.PowerOff() + return nil + } + + d.PowerOn() + return nil } // SetBlocking changes the blocking flag of the device @@ -303,16 +346,16 @@ func (d *Device) SetBlocking(blocking bool) { d.blocking = blocking } -// xy chages the coordinates according to the rotation +// xy changes the coordinates according to the rotation func (d *Device) xy(x, y int16) (int16, int16) { switch d.rotation { - case NO_ROTATION: + case drivers.Rotation0: return x, y - case ROTATION_90: + case drivers.Rotation90: return d.width - y - 1, x - case ROTATION_180: + case drivers.Rotation180: return d.width - x - 1, d.height - y - 1 - case ROTATION_270: + case drivers.Rotation270: return y, d.height - x - 1 } return x, y From 7dbca2a5434937cf5c7b30d924134891d66acd70 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Thu, 9 May 2024 12:42:59 +0200 Subject: [PATCH 021/137] uc8151: correct DrawBitmap() also refactor SendCommand() and SendData() for clarity Signed-off-by: deadprogram --- uc8151/uc8151.go | 48 +++++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/uc8151/uc8151.go b/uc8151/uc8151.go index 2d4c03b..1775f1b 100644 --- a/uc8151/uc8151.go +++ b/uc8151/uc8151.go @@ -122,7 +122,6 @@ func (d *Device) Configure(cfg Config) { d.SendCommand(POF) d.WaitUntilIdle() - } // Reset resets the device @@ -146,29 +145,23 @@ func (d *Device) PowerOn() { // SendCommand sends a command to the display func (d *Device) SendCommand(command uint8) { - d.sendDataCommand(true, command) + d.dc.Low() + d.cs.Low() + d.bus.Transfer(command) + d.cs.High() } // SendData sends a data byte to the display -func (d *Device) SendData(data uint8) { - d.sendDataCommand(false, data) -} - -// sendDataCommand sends image data or a command to the screen -func (d *Device) sendDataCommand(isCommand bool, data uint8) { - if isCommand { - d.dc.Low() - } else { - d.dc.High() - } +func (d *Device) SendData(data ...uint8) { + d.dc.High() d.cs.Low() - d.bus.Transfer(data) + d.bus.Tx(data, nil) d.cs.High() } // SetPixel modifies the internal buffer in a single pixel. // The display have 2 colors: black and white -// We use RGBA(0,0,0, 255) as white (transparent) +// We use RGBA(0, 0, 0) as white (transparent) // Anything else as black func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { x, y = d.xy(x, y) @@ -177,22 +170,23 @@ func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { return } byteIndex := x/8 + y*(d.width/8) - if c.R == 0 && c.G == 0 && c.B == 0 { // TRANSPARENT / WHITE - d.buffer[byteIndex] &^= 0x80 >> uint8(x%8) - } else { // WHITE / EMPTY + if c.R != 0 || c.G != 0 || c.B != 0 { d.buffer[byteIndex] |= 0x80 >> uint8(x%8) + } else { + d.buffer[byteIndex] &^= 0x80 >> uint8(x%8) } } // DrawBitmap copies the bitmap to the screen at the given coordinates. func (d *Device) DrawBitmap(x, y int16, bitmap pixel.Image[pixel.Monochrome]) error { - width, height := bitmap.Size() - if x < 0 || x+int16(width) > d.width || y < 0 || y+int16(height) > d.height { + dw, dh := d.Size() + bw, bh := bitmap.Size() + if x < 0 || x+int16(bw) > dw || y < 0 || y+int16(bh) > dh { return errOutOfRange } - for i := 0; i < width; i++ { - for j := 0; j < height; j++ { + for i := 0; i < bw; i++ { + for j := 0; j < bh; j++ { d.SetPixel(x+int16(i), y+int16(j), bitmap.Get(i, j).RGBA()) } } @@ -205,12 +199,11 @@ func (d *Device) Display() error { if d.blocking { d.WaitUntilIdle() } - d.SendCommand(PON) + d.PowerOn() + d.SendCommand(PTOU) d.SendCommand(DTM2) - for i := uint32(0); i < d.bufferLength; i++ { - d.SendData(d.buffer[i]) - } + d.SendData(d.buffer...) d.SendCommand(DSP) d.SendCommand(DRF) @@ -218,6 +211,7 @@ func (d *Device) Display() error { d.WaitUntilIdle() d.PowerOff() } + return nil } @@ -295,7 +289,7 @@ func (d *Device) ClearDisplay() { // WaitUntilIdle waits until the display is ready func (d *Device) WaitUntilIdle() { for !d.busy.Get() { - time.Sleep(100 * time.Millisecond) + time.Sleep(10 * time.Millisecond) } } From 2a621dc3b16c6ed1a62ca09da422f146360222e6 Mon Sep 17 00:00:00 2001 From: gmsec <18008688+gmonarque@users.noreply.github.com> Date: Sun, 12 May 2024 10:42:03 +0200 Subject: [PATCH 022/137] Add driver for MCP9808 i2c temperature sensor (#676) mcp9808: add support for temperature sensor --- examples/mcp9808/main.go | 48 ++++++++++++++++++++ go.mod | 2 + mcp9808/mcp9808.go | 98 ++++++++++++++++++++++++++++++++++++++++ mcp9808/registers.go | 53 ++++++++++++++++++++++ smoketest.sh | 1 + 5 files changed, 202 insertions(+) create mode 100644 examples/mcp9808/main.go create mode 100644 mcp9808/mcp9808.go create mode 100644 mcp9808/registers.go diff --git a/examples/mcp9808/main.go b/examples/mcp9808/main.go new file mode 100644 index 0000000..7b2c5ea --- /dev/null +++ b/examples/mcp9808/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "machine" + "time" + + "tinygo.org/x/drivers/mcp9808" +) + +func main() { + + //tinygo monitor + time.Sleep(time.Millisecond * 5000) + + //Configure I2C (in this case, I2C0 on RPI Pico), and wire the module accordingly + machine.I2C0.Configure(machine.I2CConfig{ + SCL: machine.GP1, + SDA: machine.GP0, + }) + + //Create sensor + sensor := mcp9808.New(machine.I2C0) + if !sensor.Connected() { + println("MCP9808 not found") + return + } else { + println("MCP9808 found") + } + + time.Sleep(time.Millisecond * 1000) + + //Set resolution + sensor.SetResolution(mcp9808.Maximum) + + time.Sleep(time.Millisecond * 1000) + + //Read temp. + temp, err := sensor.ReadTemperature() + if err != nil { + println("MCP9808 error reading temperature") + println(err.Error()) + return + } else { + fmt.Printf("Temperature: %.2f \n", temp) + } + return +} diff --git a/go.mod b/go.mod index 0fa3076..366deda 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,8 @@ module tinygo.org/x/drivers go 1.18 +replace tinygo.org/x/drivers/mcp9808 => /home/kasterby/Documents/drivers/mcp9808 + require ( github.com/eclipse/paho.mqtt.golang v1.2.0 github.com/frankban/quicktest v1.10.2 diff --git a/mcp9808/mcp9808.go b/mcp9808/mcp9808.go new file mode 100644 index 0000000..a4ec998 --- /dev/null +++ b/mcp9808/mcp9808.go @@ -0,0 +1,98 @@ +// Package mcp9808 implements a driver for the MCP9808 High Accuracy I2C Temperature Sensor +// +// Datasheet: https://cdn-shop.adafruit.com/datasheets/MCP9808.pdf +// Module: https://www.adafruit.com/product/1782 +// Only implemented: temperature reading, resolution read & set +package mcp9808 + +import ( + "encoding/binary" + "errors" + + "tinygo.org/x/drivers" +) + +type Device struct { + bus drivers.I2C + Address uint16 +} + +func New(bus drivers.I2C) Device { + return Device{bus, MCP9808_I2CADDR_DEFAULT} +} + +func (d *Device) Connected() bool { + data := make([]byte, 2) + d.Read(MCP9808_REG_DEVICE_ID, &data) + return binary.BigEndian.Uint16(data) == MCP9808_DEVICE_ID +} + +func (d *Device) ReadTemperature() (float64, error) { + data := make([]byte, 2) + var temp float64 + if err := d.Read(MCP9808_REG_AMBIENT_TEMP, &data); err != nil { + return 0, err + } + + data[0] = data[0] & 0x1F + if data[0]&0x10 == 0x10 { + data[0] = data[0] & 0x0F + temp = float64(data[0])*16 + float64(data[1])/16.0 - 256 + } + temp = float64(data[0])*16 + float64(data[1])/16.0 + return temp, nil +} + +func (d *Device) ReadResolution() (resolution, error) { + data := make([]byte, 2) + err := d.Read(MCP9808_REG_RESOLUTION, &data) + if err != nil { + return 0, err + } + switch data[0] { + case 0: + return Low, nil + case 1: + return Medium, nil + case 2: + return High, nil + case 3: + return Maximum, nil + + default: + return 0, errors.New("unknown resolution") + } +} + +func (d *Device) SetResolution(r resolution) error { + switch r { + case Low: + if err := d.Write(MCP9808_REG_RESOLUTION, []byte{0x00}); err != nil { + return err + } + case Medium: + if err := d.Write(MCP9808_REG_RESOLUTION, []byte{0x01}); err != nil { + return err + } + case High: + if err := d.Write(MCP9808_REG_RESOLUTION, []byte{0x02}); err != nil { + return err + } + case Maximum: + if err := d.Write(MCP9808_REG_RESOLUTION, []byte{0x03}); err != nil { + return err + } + default: + return nil + } + return nil +} + +func (d *Device) Write(register byte, data []byte) error { + buf := append([]byte{register}, data...) + return d.bus.Tx(d.Address, buf, nil) +} + +func (d *Device) Read(register byte, data *[]byte) error { + return d.bus.Tx(d.Address, []byte{register}, *data) +} diff --git a/mcp9808/registers.go b/mcp9808/registers.go new file mode 100644 index 0000000..a66d094 --- /dev/null +++ b/mcp9808/registers.go @@ -0,0 +1,53 @@ +// Package mcp9808 implements a driver for the MCP9808 High Accuracy I2C Temperature Sensor +// +// Datasheet: https://cdn-shop.adafruit.com/datasheets/MCP9808.pdf +// Module: https://www.adafruit.com/product/1782 +package mcp9808 + +// Constants/addresses used for I2C. +const ( + MCP9808_DEVICE_ID = 0x0400 + MCP9808_MANUF_ID = 0x0054 + + MCP9808_I2CADDR_DEFAULT = 0x18 //default I2C address + MCP9808_REG_CONFIG = 0x01 //MCP9808 config register + + MCP9808_REG_CONFIG_SHUTDOWN = 0x0100 //shutdown config + MCP9808_REG_CONFIG_CRITLOCKED = 0x0080 //critical trip lock + MCP9808_REG_CONFIG_WINLOCKED = 0x0040 //alarm window lock + MCP9808_REG_CONFIG_INTCLR = 0x0020 //interrupt clear + MCP9808_REG_CONFIG_ALERTSTAT = 0x0010 //alert output status + MCP9808_REG_CONFIG_ALERTCTRL = 0x0008 //alert output control + MCP9808_REG_CONFIG_ALERTSEL = 0x0004 //alert output select + MCP9808_REG_CONFIG_ALERTPOL = 0x0002 //alert output polarity + MCP9808_REG_CONFIG_ALERTMODE = 0x0001 //alert output mode + + MCP9808_REG_UPPER_TEMP = 0x02 //upper alert boundary + MCP9808_REG_LOWER_TEMP = 0x03 //lower alert boundery + MCP9808_REG_CRIT_TEMP = 0x04 //critical temperature + MCP9808_REG_AMBIENT_TEMP = 0x05 //ambient temperature + MCP9808_REG_MANUF_ID = 0x06 //manufacturer ID + MCP9808_REG_DEVICE_ID = 0x07 //device ID + MCP9808_REG_RESOLUTION = 0x08 //resolution +) + +/* +======= ============ ============== +value resolution reading Time +======= ============ ============== + + 0 0.5°C 30 ms + 1 0.25°C 65 ms + 2 0.125°C 130 ms + 3 0.0625°C 250 ms + +======= ============ ============== +*/ +type resolution uint8 + +const ( + Low resolution = iota + Medium + High + Maximum +) diff --git a/smoketest.sh b/smoketest.sh index b7530b2..a10c9fe 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -130,6 +130,7 @@ tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/n tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mpu9150/main.go tinygo build -size short -o ./build/test.hex -target=macropad-rp2040 ./examples/sh1106/macropad_spi tinygo build -size short -o ./build/test.hex -target=macropad-rp2040 ./examples/encoders/quadrature-interrupt +tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mcp9808/main.go # network examples (espat) tinygo build -size short -o ./build/test.hex -target=challenger-rp2040 ./examples/net/ntpclient/ # network examples (wifinina) From 9063f463133141ecdabe75ce8e3f5ff326d527ba Mon Sep 17 00:00:00 2001 From: deadprogram Date: Fri, 10 May 2024 17:57:21 +0200 Subject: [PATCH 023/137] uc8151: improvements to speed and also add flicker-free mode based on @antirez code example Signed-off-by: deadprogram --- examples/uc8151/main.go | 51 +++-- uc8151/lut.go | 30 +++ uc8151/registers.go | 8 +- uc8151/uc8151.go | 440 ++++++++++++---------------------------- 4 files changed, 199 insertions(+), 330 deletions(-) create mode 100644 uc8151/lut.go diff --git a/examples/uc8151/main.go b/examples/uc8151/main.go index c0e7520..1ee778b 100644 --- a/examples/uc8151/main.go +++ b/examples/uc8151/main.go @@ -3,6 +3,7 @@ package main import ( "image/color" "machine" + "time" "tinygo.org/x/drivers" "tinygo.org/x/drivers/uc8151" @@ -15,30 +16,56 @@ func main() { led = machine.LED led.Configure(machine.PinConfig{Mode: machine.PinOutput}) machine.SPI0.Configure(machine.SPIConfig{ - Frequency: 12000000, + Frequency: 12 * machine.MHz, SCK: machine.EPD_SCK_PIN, SDO: machine.EPD_SDO_PIN, }) display = uc8151.New(machine.SPI0, machine.EPD_CS_PIN, machine.EPD_DC_PIN, machine.EPD_RESET_PIN, machine.EPD_BUSY_PIN) display.Configure(uc8151.Config{ - Rotation: drivers.Rotation270, - Speed: uc8151.MEDIUM, - Blocking: true, + Rotation: drivers.Rotation270, + Speed: uc8151.TURBO, + FlickerFree: true, + Blocking: false, }) black := color.RGBA{1, 1, 1, 255} - display.ClearBuffer() - display.Display() - for i := int16(0); i < 37; i++ { - for j := int16(0); j < 16; j++ { - if (i+j)%2 == 0 { - showRect(i*8, j*8, 8, 8, black) + display.ClearDisplay() + + mod := int16(1) + for { + // checkerboard + for i := int16(0); i < 11; i++ { + if mod == 1 { + mod = 0 + } else { + mod = 1 + } + + display.ClearBuffer() + for i := int16(0); i < 37; i++ { + for j := int16(0); j < 16; j++ { + if (i+j)%2 == mod { + showRect(i*8, j*8, 8, 8, black) + } + } + } + display.Display() + time.Sleep(500 * time.Millisecond) + } + + // moving line + for i := int16(16); i < 21; i++ { + display.ClearBuffer() + for j := int16(0); j < 16; j++ { + if (i+j)%2 == 0 { + showRect(i*8, j*8, 8, 8, black) + } + display.Display() + time.Sleep(250 * time.Millisecond) } } } - - display.Display() } func showRect(x int16, y int16, w int16, h int16, c color.RGBA) { diff --git a/uc8151/lut.go b/uc8151/lut.go new file mode 100644 index 0000000..392488b --- /dev/null +++ b/uc8151/lut.go @@ -0,0 +1,30 @@ +package uc8151 + +// LUTType is the look-up table for the display +type LUTType [42]uint8 + +type LUTSet struct { + VCOM LUTType + WW LUTType + BW LUTType + WB LUTType + BB LUTType +} + +func (lut *LUTType) Clear() { + for i := range lut { + lut[i] = 0 + } +} + +func (lut *LUTType) SetRow(row int, pat uint8, dur [4]uint8, rep uint8) error { + index := row * 6 + lut[index] = pat + lut[index+1] = dur[0] + lut[index+2] = dur[1] + lut[index+3] = dur[2] + lut[index+4] = dur[3] + lut[index+5] = rep + + return nil +} diff --git a/uc8151/registers.go b/uc8151/registers.go index c4170c8..407d7a4 100644 --- a/uc8151/registers.go +++ b/uc8151/registers.go @@ -150,7 +150,9 @@ const ( ROTATION_270 = drivers.Rotation270 DEFAULT Speed = 0 - MEDIUM Speed = 1 - FAST Speed = 2 - TURBO Speed = 3 + SLOW Speed = 1 + MEDIUM Speed = 2 + FAST Speed = 3 + FASTER Speed = 4 + TURBO Speed = 5 ) diff --git a/uc8151/uc8151.go b/uc8151/uc8151.go index 1775f1b..0c658d1 100644 --- a/uc8151/uc8151.go +++ b/uc8151/uc8151.go @@ -1,6 +1,7 @@ // Package uc8151 implements a driver for e-ink displays controlled by UC8151 // // Inspired by https://github.com/pimoroni/pimoroni-pico/blob/main/drivers/uc8151/uc8151.cpp +// Additional inspiration from https://github.com/antirez/uc8151_micropython // Datasheet: https://www.buydisplay.com/download/ic/UC8151C.pdf package uc8151 // import "tinygo.org/x/drivers/uc8151" @@ -19,31 +20,35 @@ var ( ) type Config struct { - Width int16 - Height int16 - Rotation drivers.Rotation // Rotation is clock-wise - Speed Speed // Value from DEFAULT, MEDIUM, FAST, TURBO - Blocking bool + Width int16 + Height int16 + Rotation drivers.Rotation // Rotation is clock-wise + Speed Speed // Value from DEFAULT, SLOW, MEDIUM, FAST, FASTER, TURBO + Blocking bool // block on calls to display or return immediately + FlickerFree bool // if we should avoid flickering + UpdateAfter int // if we are using flicker-free mode, how often we should update the screen } type Device struct { - bus drivers.SPI - cs machine.Pin - dc machine.Pin - rst machine.Pin - busy machine.Pin - width int16 - height int16 - buffer []uint8 - bufferLength uint32 - rotation drivers.Rotation - speed Speed - blocking bool + bus drivers.SPI + cs machine.Pin + dc machine.Pin + rst machine.Pin + busy machine.Pin + width int16 + height int16 + buffer []uint8 + bufferLength uint32 + rotation drivers.Rotation + speed Speed + blocking bool + flickerFree bool + updateCount, updateAfter int } type Speed uint8 -// New returns a new epd2in13x driver. Pass in a fully configured SPI bus. +// New returns a new uc8151 driver. Pass in a fully configured SPI bus. func New(bus drivers.SPI, csPin, dcPin, rstPin, busyPin machine.Pin) Device { csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) @@ -73,6 +78,8 @@ func (d *Device) Configure(cfg Config) { d.rotation = cfg.Rotation d.speed = cfg.Speed d.blocking = cfg.Blocking + d.flickerFree = cfg.FlickerFree + d.updateAfter = cfg.UpdateAfter d.bufferLength = (uint32(d.width) * uint32(d.height)) / 8 d.buffer = make([]uint8, d.bufferLength) for i := uint32(0); i < d.bufferLength; i++ { @@ -88,14 +95,14 @@ func (d *Device) Configure(cfg Config) { d.SendData(RES_128x296 | LUT_REG | FORMAT_BW | SHIFT_RIGHT | BOOSTER_ON | RESET_NONE | SCAN_UP) } - d.SetLUT(d.speed) + d.SetLUT(d.speed, d.flickerFree) d.SendCommand(PWR) d.SendData(VDS_INTERNAL | VDG_INTERNAL) - d.SendData(VCOM_VG | VGHL_16V) - d.SendData(0x2B) - d.SendData(0x2B) - d.SendData(0x2B) + d.SendData(VCOM_VD | VGHL_16V) + d.SendData(0b100110) // +10v VDH + d.SendData(0b100110) // -10v VDL + d.SendData(0b000011) // VDHR default (For red pixels, not used here) d.SendCommand(PON) d.WaitUntilIdle() @@ -106,7 +113,7 @@ func (d *Device) Configure(cfg Config) { d.SendData(START_10MS | STRENGTH_3 | OFF_6_58US) d.SendCommand(PFS) - d.SendData(FRAMES_1) + d.SendData(FRAMES_4) d.SendCommand(TSE) d.SendData(TEMP_INTERNAL | OFFSET_0) @@ -115,7 +122,7 @@ func (d *Device) Configure(cfg Config) { d.SendData(0x22) d.SendCommand(CDI) - d.SendData(0x4C) // 4C //5C + d.SendData(0b11_00_1100) d.SendCommand(PLL) d.SendData(HZ_100) @@ -199,6 +206,15 @@ func (d *Device) Display() error { if d.blocking { d.WaitUntilIdle() } + + if d.flickerFree && d.updateAfter != 0 && d.updateCount%d.updateAfter == 0 { + // we need full refresh here + d.SetLUT(MEDIUM, false) + } else { + d.SetLUT(d.speed, d.flickerFree) + } + d.updateCount++ + d.PowerOn() d.SendCommand(PTOU) @@ -207,6 +223,9 @@ func (d *Device) Display() error { d.SendCommand(DSP) d.SendCommand(DRF) + + d.SetLUT(d.speed, d.flickerFree) + if d.blocking { d.WaitUntilIdle() d.PowerOff() @@ -282,6 +301,12 @@ func (d *Device) DisplayRect(x int16, y int16, width int16, height int16) error // ClearDisplay erases the device SRAM func (d *Device) ClearDisplay() { + ff := d.flickerFree + d.flickerFree = false + defer func() { + d.flickerFree = ff + }() + d.ClearBuffer() d.Display() } @@ -375,296 +400,81 @@ func (d *Device) Invert(invert bool) { } } -// SetLUT sets the look up tables for full or partial updates -func (d *Device) SetLUT(speed Speed) { - switch speed { - case MEDIUM: - var lut = [44]uint8{ - 0x00, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x00, 0x23, 0x23, 0x00, 0x00, 0x02, - 0x00, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, - } - d.SendCommand(LUT_VCOM) - for i := 0; i < 44; i++ { - d.SendData(lut[i]) - } - lut = [44]uint8{ - 0x54, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x60, 0x23, 0x23, 0x00, 0x00, 0x02, - 0xa8, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_WW) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } +// SetLUT sets the look up tables for full or partial updates based on +// the speed and flicker-free mode. +// Based on code from https://github.com/antirez/uc8151_micropython +func (d *Device) SetLUT(speed Speed, flickerFree bool) error { + var lut LUTSet - lut = [44]uint8{ - 0x54, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x60, 0x23, 0x23, 0x00, 0x00, 0x02, - 0xa8, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_BW) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0xa8, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x60, 0x23, 0x23, 0x00, 0x00, 0x02, - 0x54, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_WB) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0xa8, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x60, 0x23, 0x23, 0x00, 0x00, 0x02, - 0x54, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_BB) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - break - case FAST: - var lut = [44]uint8{ - 0x00, 0x04, 0x04, 0x07, 0x00, 0x01, - 0x00, 0x0c, 0x0c, 0x00, 0x00, 0x02, - 0x00, 0x04, 0x04, 0x07, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, - } - d.SendCommand(LUT_VCOM) - for i := 0; i < 44; i++ { - d.SendData(lut[i]) - } - lut = [44]uint8{ - 0x54, 0x04, 0x04, 0x07, 0x00, 0x01, - 0x60, 0x0c, 0x0c, 0x00, 0x00, 0x02, - 0xa8, 0x04, 0x04, 0x07, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_WW) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0x54, 0x04, 0x04, 0x07, 0x00, 0x01, - 0x60, 0x0c, 0x0c, 0x00, 0x00, 0x02, - 0xa8, 0x04, 0x04, 0x07, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_BW) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0xa8, 0x04, 0x04, 0x07, 0x00, 0x01, - 0x60, 0x0c, 0x0c, 0x00, 0x00, 0x02, - 0x54, 0x04, 0x04, 0x07, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_WB) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0xa8, 0x04, 0x04, 0x07, 0x00, 0x01, - 0x60, 0x0c, 0x0c, 0x00, 0x00, 0x02, - 0x54, 0x04, 0x04, 0x07, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_BB) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - break - case TURBO: - var lut = [44]uint8{ - 0x00, 0x01, 0x01, 0x02, 0x00, 0x01, - 0x00, 0x02, 0x02, 0x00, 0x00, 0x02, - 0x00, 0x02, 0x02, 0x03, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, - } - d.SendCommand(LUT_VCOM) - for i := 0; i < 44; i++ { - d.SendData(lut[i]) - } - lut = [44]uint8{ - 0x54, 0x01, 0x01, 0x02, 0x00, 0x01, - 0x60, 0x02, 0x02, 0x00, 0x00, 0x02, - 0xa8, 0x02, 0x02, 0x03, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_WW) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0x54, 0x01, 0x01, 0x02, 0x00, 0x01, - 0x60, 0x02, 0x02, 0x00, 0x00, 0x02, - 0xa8, 0x02, 0x02, 0x03, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_BW) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0xa8, 0x01, 0x01, 0x02, 0x00, 0x01, - 0x60, 0x02, 0x02, 0x00, 0x00, 0x02, - 0x54, 0x02, 0x02, 0x03, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_WB) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0xa8, 0x01, 0x01, 0x02, 0x00, 0x01, - 0x60, 0x02, 0x02, 0x00, 0x00, 0x02, - 0x54, 0x02, 0x02, 0x03, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_BB) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - break - default: - var lut = [44]uint8{ - 0x00, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x00, 0x8c, 0x8c, 0x00, 0x00, 0x04, - 0x00, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, - } - d.SendCommand(LUT_VCOM) - for i := 0; i < 44; i++ { - d.SendData(lut[i]) - } - lut = [44]uint8{ - 0x54, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x60, 0x8c, 0x8c, 0x00, 0x00, 0x04, - 0xa8, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_WW) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0x54, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x60, 0x8c, 0x8c, 0x00, 0x00, 0x04, - 0xa8, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_BW) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0xa8, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x60, 0x8c, 0x8c, 0x00, 0x00, 0x04, - 0x54, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_WB) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0xa8, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x60, 0x8c, 0x8c, 0x00, 0x00, 0x04, - 0x54, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_BB) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - break + // Num. of frames for single direction change. + period := 64 + p := uint8(period / (2 ^ (int(speed) - 1))) + if p < 1 { + p = 1 } + + // Num. of frames for back-and-forth change. + hperiod := period % 2 + hp := uint8(hperiod / (2 ^ (int(speed) - 1))) + if hp < 1 { + hp = 1 + } + + if speed < FAST && !flickerFree { + // For low speed everything is charge-neutral, even WB/BW. + + // Phase 1: long go-inverted-color. + lut.VCOM.SetRow(0, 0x00, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02) + lut.BW.SetRow(0, 0b01_000000, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02) + lut.WB.SetRow(0, 0b10_000000, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02) + + // Phase 2: short ping/pong. + lut.VCOM.SetRow(1, 0x00, [4]uint8{hp, hp, 0x00, 0x00}, 0x02) + lut.BW.SetRow(1, 0b10_01_0000, [4]uint8{hp, hp, 0x00, 0x00}, 0x01) + lut.WB.SetRow(1, 0b01_10_0000, [4]uint8{hp, hp, 0x00, 0x00}, 0x01) + + // Phase 3: long go-target-color. + lut.VCOM.SetRow(2, 0x00, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02) + lut.BW.SetRow(2, 0b10_000000, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02) + lut.WB.SetRow(2, 0b01_000000, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02) + + // For this speed, we use the same LUTs for WW/BB as well. + copy(lut.WW[:], lut.BW[:]) + copy(lut.BB[:], lut.WB[:]) + } else { + // Speed >= FAST + // For greater than 3 we use non charge-neutral LUTs for WB/BW + // since the inpulse is short and it gets reversed when the + // pixel changes color, so that's not a problem for the display, + // however we still need to use charge-neutral LUTs for WW/BB. + lut.VCOM.SetRow(0, 0x00, [4]uint8{p, p, p, p}, 0x01) + lut.BW.SetRow(0, 0b10_00_00_00, [4]uint8{p * 4, 0x00, 0x00, 0x00}, 0x01) + lut.WB.SetRow(0, 0b01_00_00_00, [4]uint8{p * 4, 0x00, 0x00, 0x00}, 0x01) + lut.WW.SetRow(0, 0b01_10_00_00, [4]uint8{p * 2, p * 2, 0x00, 0x00}, 0x01) + lut.BB.SetRow(0, 0b10_01_00_00, [4]uint8{p * 2, p * 2, 0x00, 0x00}, 0x01) + } + + if flickerFree { + // If no flickering mode is enabled, we use an empty + // waveform BB and WW. The screen will need to be periodically fully refreshed. + lut.WW.Clear() + lut.BB.Clear() + } + + d.SendCommand(LUT_VCOM) + d.SendData(append(lut.VCOM[:], []uint8{0, 0}...)...) + + d.SendCommand(LUT_BW) + d.SendData(lut.BW[:]...) + + d.SendCommand(LUT_WB) + d.SendData(lut.WB[:]...) + + d.SendCommand(LUT_WW) + d.SendData(lut.WW[:]...) + + d.SendCommand(LUT_BB) + d.SendData(lut.BB[:]...) + + return nil } From 831982ad33ee72726d19ac8bd0b95c5b38ddd4bb Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 21 May 2024 19:16:22 +0200 Subject: [PATCH 024/137] servo: add function SetAngle() to simplify API for most common use case Signed-off-by: deadprogram --- examples/servo/servo.go | 32 +++++++++++++++++++------------- servo/servo.go | 24 +++++++++++++++++++++++- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/examples/servo/servo.go b/examples/servo/servo.go index c986d84..a38d25d 100644 --- a/examples/servo/servo.go +++ b/examples/servo/servo.go @@ -25,19 +25,25 @@ func main() { return } - println("setting to 0°") - s.SetMicroseconds(1000) - time.Sleep(3 * time.Second) - - println("setting to 45°") - s.SetMicroseconds(1500) - time.Sleep(3 * time.Second) - - println("setting to 90°") - s.SetMicroseconds(2000) - time.Sleep(3 * time.Second) - for { - time.Sleep(time.Second) + println("setting to 0°") + s.SetAngle(0) + time.Sleep(3 * time.Second) + + println("setting to 45°") + s.SetAngle(45) + time.Sleep(3 * time.Second) + + println("setting to 90°") + s.SetAngle(90) + time.Sleep(3 * time.Second) + + println("setting to 135°") + s.SetAngle(135) + time.Sleep(3 * time.Second) + + println("setting to 180°") + s.SetAngle(180) + time.Sleep(3 * time.Second) } } diff --git a/servo/servo.go b/servo/servo.go index 5be92ac..73ef8e0 100644 --- a/servo/servo.go +++ b/servo/servo.go @@ -1,6 +1,12 @@ package servo -import "machine" +import ( + "machine" + + "errors" +) + +var ErrInvalidAngle = errors.New("servo: invalid angle") // PWM is the interface necessary for controlling typical servo motors. type PWM interface { @@ -80,3 +86,19 @@ func (s Servo) SetMicroseconds(microseconds int16) { value := uint64(s.pwm.Top()) * uint64(microseconds) / (pwmPeriod / 1000) s.pwm.Set(s.channel, uint32(value)) } + +// SetAngle sets the angle of the servo in degrees. The angle should be between +// 0 and 180, where 0 is the minimum angle and 180 is the maximum angle. +// This function should work for most servos, but if it doesn't work for yours +// you can use SetMicroseconds directly instead. +func (s Servo) SetAngle(angle int) error { + if angle < 0 || angle > 180 { + return ErrInvalidAngle + } + + // 0° is 1000µs, 180° is 2000µs. See explanation in SetMicroseconds. + microseconds := angle*1000/180 + 1000 + s.SetMicroseconds(int16(microseconds)) + + return nil +} From 7d983647ad1444074a071837fb7bc1710bcc449a Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sat, 25 May 2024 15:42:11 +0200 Subject: [PATCH 025/137] pixel: fix Image[Monochrome].Set for larger images For bigger images, the pixel index might not fit in a int16. Therefore, int is needed during the calculation. While fixing this bug, I've added a few tests that verify the Image implementation by creating images, filling them with random data, and then checking whether they still contain the same data. This test failed before the patch. --- pixel/image.go | 6 ++-- pixel/image_test.go | 68 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/pixel/image.go b/pixel/image.go index 0a3f727..1a1d26f 100644 --- a/pixel/image.go +++ b/pixel/image.go @@ -104,9 +104,9 @@ func (img Image[T]) setPixel(index int, c T) { switch { case zeroColor.BitsPerPixel() == 1: // Monochrome. - x := int16(index) % img.width - y := int16(index) / img.width - offset := x + (y/8)*img.width + x := index % int(img.width) + y := index / int(img.width) + offset := x + (y/8)*int(img.width) ptr := (*byte)(unsafe.Add(img.data, offset)) if c != zeroColor { *((*byte)(ptr)) |= 1 << uint8(y%8) diff --git a/pixel/image_test.go b/pixel/image_test.go index e7f165e..bb7afc8 100644 --- a/pixel/image_test.go +++ b/pixel/image_test.go @@ -1,7 +1,9 @@ package pixel_test import ( + goimage "image" "image/color" + "math/rand" "testing" "tinygo.org/x/drivers/pixel" @@ -94,3 +96,69 @@ func TestImageMonochrome(t *testing.T) { } } } + +// Test pixel formats by filling them with noise and checking whether they +// contain the same data afterwards. +func TestImageNoise(t *testing.T) { + t.Run("RGB888", func(t *testing.T) { + testImageNoise[pixel.RGB888](t) + }) + t.Run("RGB565BE", func(t *testing.T) { + testImageNoise[pixel.RGB565BE](t) + }) + t.Run("RGB555", func(t *testing.T) { + testImageNoise[pixel.RGB555](t) + }) + t.Run("RGB444BE", func(t *testing.T) { + testImageNoise[pixel.RGB444BE](t) + }) + t.Run("Monochrome", func(t *testing.T) { + testImageNoise[pixel.Monochrome](t) + }) +} + +func testImageNoise[T pixel.Color](t *testing.T) { + // Create an image of a random width/height for extra testing. + width := rand.Int()%500 + 10 + height := rand.Int()%500 + 10 + t.Log("image size:", width, height) + + // Create two images: the to-be-tested image object and a reference image. + img := pixel.NewImage[T](width, height) + ref := goimage.NewRGBA(goimage.Rect(0, 0, width, height)) + + // Fill the two images with noise. + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + // Set a random color in both images. + c := pixel.NewColor[T](uint8(rand.Uint32()), uint8(rand.Uint32()), uint8(rand.Uint32())) + img.Set(x, y, c) + ref.Set(x, y, c.RGBA()) + } + } + + // Compare the two images. They should match. + mismatch := 0 + firstX := 0 + firstY := 0 + var firstExpected, firstActual color.RGBA + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + c := img.Get(x, y).RGBA() + r2, g2, b2, _ := ref.At(x, y).RGBA() + c2 := color.RGBA{R: uint8(r2 >> 8), G: uint8(g2 >> 8), B: uint8(b2 >> 8), A: 255} + if c != c2 { + mismatch++ + if mismatch == 1 { + firstX = x + firstY = y + firstExpected = c + firstActual = c2 + } + } + } + } + if mismatch != 0 { + t.Errorf("mismatch found: %d pixels are different (first diff at (%d, %d), expected %v, actual %v)", mismatch, firstX, firstY, firstExpected, firstActual) + } +} From 4a0bcba87c4a47d8291807d1d6666a232c66f3ea Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 10 Jun 2024 13:16:45 +0200 Subject: [PATCH 026/137] epd2in66b: Waveshare 2.66inch E-Paper Display Module (B) for Raspberry Pi Pico (#673) epd2in66b: add working driver Co-authored-by: Thomas Richner --- Makefile | 2 +- examples/waveshare-epd/epd2in66b/main.go | 84 +++++++ smoketest.sh | 1 + waveshare-epd/epd2in66b/dev.go | 287 +++++++++++++++++++++++ waveshare-epd/epd2in66b/dev_pico.go | 34 +++ waveshare-epd/epd2in66b/dev_test.go | 92 ++++++++ 6 files changed, 499 insertions(+), 1 deletion(-) create mode 100644 examples/waveshare-epd/epd2in66b/main.go create mode 100644 waveshare-epd/epd2in66b/dev.go create mode 100644 waveshare-epd/epd2in66b/dev_pico.go create mode 100644 waveshare-epd/epd2in66b/dev_test.go diff --git a/Makefile b/Makefile index 112edbb..c05ef64 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ rwildcard=$(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst # Recursively find all *_test.go files from cwd & reduce to unique dir names HAS_TESTS = $(sort $(dir $(call rwildcard,,*_test.go))) # Exclude anything we explicitly don't want to test for whatever reason -EXCLUDE_TESTS = image +EXCLUDE_TESTS = image waveshare-epd/epd2in66b TESTS = $(filter-out $(addsuffix /%,$(EXCLUDE_TESTS)),$(HAS_TESTS)) unit-test: diff --git a/examples/waveshare-epd/epd2in66b/main.go b/examples/waveshare-epd/epd2in66b/main.go new file mode 100644 index 0000000..a9c75b6 --- /dev/null +++ b/examples/waveshare-epd/epd2in66b/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "image/color" + "machine" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/waveshare-epd/epd2in66b" + "tinygo.org/x/tinyfont" + "tinygo.org/x/tinyfont/freemono" +) + +var ( + black = color.RGBA{0, 0, 0, 0xff} + white = color.RGBA{0xff, 0xff, 0xff, 0xff} + red = color.RGBA{0xff, 0, 0, 0xff} +) + +func main() { + machine.Serial.Configure(machine.UARTConfig{}) + time.Sleep(2 * time.Second) + + machine.SPI1.Configure(machine.SPIConfig{ + Frequency: epd2in66b.Baudrate, + }) + + println("started") + + // in case you have a Pico module, you can directly use + // dev, err := epd2in66b.NewPicoModule() + + display := epd2in66b.New(machine.SPI1) + + cfg := epd2in66b.Config{ + DataPin: machine.GP8, + ChipSelectPin: machine.GP9, + ResetPin: machine.GP12, + BusyPin: machine.GP13, + } + err := display.Configure(cfg) + if err != nil { + panic(err) + } + + err = display.Reset() + if err != nil { + panic(err) + } + + println("draw checkerboard") + drawCheckerBoard(&display) + + println("draw 'hello'") + tinyfont.WriteLineRotated(&display, &freemono.Bold24pt7b, 40, 10, "Hello!", white, tinyfont.ROTATION_90) + tinyfont.WriteLineRotated(&display, &freemono.Bold12pt7b, 10, 10, "tinygo rocks", white, tinyfont.ROTATION_90) + err = display.Display() + if err != nil { + panic(err) + } +} + +func drawCheckerBoard(display drivers.Displayer) { + s := 8 + width, height := display.Size() + for x := 0; x <= int(width)-s; x += s { + for y := 0; y <= int(height)-s; y += s { + c := red + if (x/s)%2 == (y/s)%2 { + c = black + } + + showRect(display, x, y, s, s, c) + } + } +} + +func showRect(display drivers.Displayer, x int, y int, w int, h int, c color.RGBA) { + for i := x; i < x+w; i++ { + for j := y; j < y+h; j++ { + display.SetPixel(int16(i), int16(j), c) + } + } +} diff --git a/smoketest.sh b/smoketest.sh index a10c9fe..569a0d8 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -80,6 +80,7 @@ tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/vl6 tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd2in13/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd2in13x/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd4in2/main.go +tinygo build -size short -o ./build/test.hex -target=pico ./examples/waveshare-epd/epd2in66b/main.go tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/ws2812 tinygo build -size short -o ./build/test.bin -target=m5stamp-c3 ./examples/ws2812 tinygo build -size short -o ./build/test.hex -target=feather-nrf52840 ./examples/is31fl3731/main.go diff --git a/waveshare-epd/epd2in66b/dev.go b/waveshare-epd/epd2in66b/dev.go new file mode 100644 index 0000000..cadff9c --- /dev/null +++ b/waveshare-epd/epd2in66b/dev.go @@ -0,0 +1,287 @@ +// Package epd2in66b implements a driver for the Waveshare 2.66inch E-Paper E-Ink Display Module (B) +// for Raspberry Pi Pico, 296×152, Red / Black / White +// Datasheet: https://files.waveshare.com/upload/e/ec/2.66inch-e-paper-b-specification.pdf +package epd2in66b + +import ( + "image/color" + "machine" + "time" + + "tinygo.org/x/drivers" +) + +const ( + displayWidth = 152 + displayHeight = 296 +) + +const Baudrate = 4_000_000 // 4 MHz + +type Config struct { + ResetPin machine.Pin + DataPin machine.Pin + ChipSelectPin machine.Pin + BusyPin machine.Pin +} + +type Device struct { + bus drivers.SPI + cs machine.Pin + dc machine.Pin + rst machine.Pin + busy machine.Pin + + blackBuffer []byte + redBuffer []byte +} + +// New allocates a new device. +// The bus is expected to be configured and ready for use. +func New(bus drivers.SPI) Device { + pixelCount := displayWidth * displayHeight + + bufLen := pixelCount / 8 + + return Device{ + bus: bus, + blackBuffer: make([]byte, bufLen), + redBuffer: make([]byte, bufLen), + } +} + +// Configure configures the device and its pins. +func (d *Device) Configure(c Config) error { + d.cs = c.ChipSelectPin + d.dc = c.DataPin + d.rst = c.ResetPin + d.busy = c.BusyPin + + d.cs.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.dc.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.rst.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.busy.Configure(machine.PinConfig{Mode: machine.PinInput}) + + return nil +} + +func (d *Device) Size() (x, y int16) { + return displayWidth, displayHeight +} + +// SetPixel modifies the internal buffer in a single pixel. +// The display has 3 colors: red, black and white +// +// - white = RGBA(255,255,255, 1-255) +// - red = RGBA(1-255,0,0,1-255) +// - Anything else as black +func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { + if x < 0 || x >= displayWidth || y < 0 || y >= displayHeight { + return + } + + bytePos, bitPos := pos(x, y, displayWidth) + + if c.R == 0xff && c.G == 0xff && c.B == 0xff && c.A > 0 { // white + set(d.blackBuffer, bytePos, bitPos) + unset(d.redBuffer, bytePos, bitPos) + } else if c.R != 0 && c.G == 0 && c.B == 0 && c.A > 0 { // red-ish + set(d.blackBuffer, bytePos, bitPos) + set(d.redBuffer, bytePos, bitPos) + } else { // black or other + unset(d.blackBuffer, bytePos, bitPos) + unset(d.redBuffer, bytePos, bitPos) + } +} + +func set(buf []byte, bytePos, bitPos int) { + buf[bytePos] |= 0x1 << bitPos +} + +func unset(buf []byte, bytePos, bitPos int) { + buf[bytePos] &^= 0x1 << bitPos +} + +func pos(x, y, stride int16) (bytePos int, bitPos int) { + p := int(x) + int(y)*int(stride) + bytePos = p / 8 + + // reverse bit position as it is reversed on the device's buffer + bitPos = 7 - p%8 + + return bytePos, bitPos +} + +func (d *Device) Display() error { + // Write RAM (Black White) / RAM 0x24 + // 1 == white, 0 == black + if err := d.sendCommandByte(0x24); err != nil { + return err + } + + if err := d.sendData(d.blackBuffer); err != nil { + return err + } + + // Write RAM (RED) / RAM 0x26) + // 0 == blank, 1 == red + if err := d.sendCommandByte(0x26); err != nil { + return err + } + + if err := d.sendData(d.redBuffer); err != nil { + return err + } + + return d.turnOnDisplay() +} + +func (d *Device) ClearBuffer() { + fill(d.redBuffer, 0x00) + fill(d.blackBuffer, 0xff) +} + +func (d *Device) turnOnDisplay() error { + // also documented as 'Master Activation' + if err := d.sendCommandByte(0x20); err != nil { + return err + } + d.WaitUntilIdle() + return nil +} + +func (d *Device) Reset() error { + d.hwReset() + d.WaitUntilIdle() + + // soft reset & set defaults + if err := d.sendCommandByte(0x12); err != nil { + return err + } + d.WaitUntilIdle() + + // data entry mode setting + if err := d.sendCommandSequence([]byte{0x11, 0x03}); err != nil { + return err + } + + if err := d.setWindow(0, displayWidth-1, 0, displayHeight-1); err != nil { + return err + } + + // display update control 1 - resolution setting + if err := d.sendCommandSequence([]byte{0x21, 0x00, 0x80}); err != nil { + return err + } + + if err := d.setCursor(0, 0); err != nil { + return err + } + d.WaitUntilIdle() + + return nil +} + +func (d *Device) setCursor(x, y uint16) error { + // Set RAM X address counter + if err := d.sendCommandSequence([]byte{0x4e, byte(x & 0x1f)}); err != nil { + return err + } + + // Set RAM Y address counter + yLo := byte(y) + yHi := byte(y>>8) & 0x1 + if err := d.sendCommandSequence([]byte{0x4f, yLo, yHi}); err != nil { + return err + } + + return nil +} + +func (d *Device) hwReset() { + d.rst.High() + time.Sleep(50 * time.Millisecond) + d.rst.Low() + time.Sleep(2 * time.Millisecond) + d.rst.High() + time.Sleep(50 * time.Millisecond) +} + +func (d *Device) setWindow(xstart, xend, ystart, yend int16) error { + // set RAM X-address start / end position + d1 := byte((xstart >> 3) & 0x1f) + d2 := byte((xend >> 3) & 0x1f) + if err := d.sendCommandSequence([]byte{0x44, d1, d2}); err != nil { + return err + } + + // set RAM Y-address start / end position + ystartLo := byte(ystart) + ystartHi := byte(ystart>>8) & 0x1 + + yendLo := byte(yend) + yendHi := byte(yend>>8) & 0x1 + + return d.sendCommandSequence([]byte{0x45, ystartLo, ystartHi, yendLo, yendHi}) +} + +func (d *Device) WaitUntilIdle() { + // give it some time to get busy + time.Sleep(50 * time.Millisecond) + + for d.busy.Get() { // high = busy + time.Sleep(10 * time.Millisecond) + } + + // give it some extra time + time.Sleep(50 * time.Millisecond) +} + +// sendCommandSequence sends the first byte in the buffer as a 'command' and all following bytes as data +func (d *Device) sendCommandSequence(seq []byte) error { + err := d.sendCommandByte(seq[0]) + if err != nil { + return err + } + + for i := 1; i < len(seq); i++ { + err = d.sendDataByte(seq[i]) + if err != nil { + return err + } + } + + return nil +} + +func (d *Device) sendCommandByte(b byte) error { + d.dc.Low() + d.cs.Low() + _, err := d.bus.Transfer(b) + d.cs.High() + return err +} + +func (d *Device) sendDataByte(b byte) error { + d.dc.High() + d.cs.Low() + _, err := d.bus.Transfer(b) + d.cs.High() + return err +} + +func (d *Device) sendData(b []byte) error { + d.dc.High() + d.cs.Low() + err := d.bus.Tx(b, nil) + d.cs.High() + return err +} + +// fill quickly fills a slice with a given value +func fill(s []byte, b byte) { + s[0] = b + for j := 1; j < len(s); j *= 2 { + copy(s[j:], s[:j]) + } +} diff --git a/waveshare-epd/epd2in66b/dev_pico.go b/waveshare-epd/epd2in66b/dev_pico.go new file mode 100644 index 0000000..eb18d1e --- /dev/null +++ b/waveshare-epd/epd2in66b/dev_pico.go @@ -0,0 +1,34 @@ +//go:build pico + +package epd2in66b + +import ( + "machine" +) + +// DefaultConfig contains the default config for the https://www.waveshare.com/wiki/Pico-ePaper-2.66 module +var DefaultConfig = Config{ + DataPin: machine.GP8, + ChipSelectPin: machine.GP9, + ResetPin: machine.GP12, + BusyPin: machine.GP13, +} + +// NewPicoModule allocates a new device backed by the https://www.waveshare.com/wiki/Pico-ePaper-2.66 module +// This will also configure the SPI1 bus and configure the device with the DefaultConfig +func NewPicoModule() (Device, error) { + spi := machine.SPI1 + + if err := spi.Configure(machine.SPIConfig{ + Frequency: Baudrate, + }); err != nil { + return Device{}, err + } + + dev := New(spi) + if err := dev.Configure(DefaultConfig); err != nil { + return dev, err + } + + return dev, nil +} diff --git a/waveshare-epd/epd2in66b/dev_test.go b/waveshare-epd/epd2in66b/dev_test.go new file mode 100644 index 0000000..e970d9a --- /dev/null +++ b/waveshare-epd/epd2in66b/dev_test.go @@ -0,0 +1,92 @@ +package epd2in66b + +import ( + _ "embed" + "fmt" + "image" + "image/color" + "image/draw" + "image/png" + "os" + "testing" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/tinyfont" + "tinygo.org/x/tinyfont/freemono" +) + +type mockBus struct{} + +func (m *mockBus) Tx(w, r []byte) error { + return nil +} + +func (m *mockBus) Transfer(b byte) (byte, error) { + return 0, nil +} + +func TestBufferDrawing(t *testing.T) { + dev := New(&mockBus{}) + + tinyfont.WriteLine(&dev, &freemono.Bold9pt7b, 10, 40, "Hello World!", color.RGBA{0xff, 0xff, 0xff, 0xff}) + + red := color.RGBA{0xff, 0, 0, 0xff} + black := color.RGBA{0xff, 0xff, 0xff, 0xff} + showRect(&dev, 10, 10, 10, 10, black) + showRect(&dev, 10, 20, 10, 10, red) + + img := toImage(&dev) + writeImage(img) +} + +func toImage(dev *Device) *image.RGBA { + red := color.RGBA{0xff, 0, 0, 0xff} + + xMax, yMax := dev.Size() + r := image.Rect(0, 0, int(xMax), int(yMax)) + container := image.NewRGBA(r) + draw.Draw(container, container.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Over) + + for x := 0; x < int(xMax); x++ { + for y := 0; y < int(yMax); y++ { + + bytePos, bitPos := pos(int16(x), int16(y), displayWidth) + + if isSet(dev.redBuffer, bytePos, bitPos) { + container.Set(x, y, red) + } else if isSet(dev.blackBuffer, bytePos, bitPos) { + container.Set(x, y, color.Black) + } + } + } + + return container +} + +func isSet(buf []byte, bytePos, bitPos int) bool { + return (buf[bytePos])&(0x1< Date: Fri, 14 Jun 2024 17:36:07 +0200 Subject: [PATCH 027/137] nit: increase verbosity of espat NetNotify error (#684) espat: increase verbosity of espat NetNotify error --- espat/espat.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/espat/espat.go b/espat/espat.go index eed674c..7696e34 100644 --- a/espat/espat.go +++ b/espat/espat.go @@ -122,7 +122,7 @@ func (d *Device) NetDisconnect() { } func (d *Device) NetNotify(cb func(netlink.Event)) { - // Not supported + fmt.Printf("\r\n%s\r\n", netlink.ErrNotSupported) } func (d *Device) GetHostByName(name string) (netip.Addr, error) { @@ -473,7 +473,6 @@ func (d *Device) parseIPD(end int) error { } // load up the socket data - //d.data = append(d.data, d.response[e+1:end]...) d.data = append(d.data, d.response[e+1:e+1+v]...) return nil } From 1bf1a11067968352afa5d7a489a13561effb2146 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Fri, 14 Jun 2024 17:56:49 +0200 Subject: [PATCH 028/137] all: prepare release v0.28.0 Signed-off-by: deadprogram --- CHANGELOG.md | 42 ++++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- version.go | 2 +- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e713bc..4408f1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,45 @@ +0.28.0 +--- +- **new devices** + - **epd2in66b** + - Waveshare 2.66inch E-Paper Display Module (B) for Raspberry Pi Pico (#673) + - **mcp9808** + - Add driver for MCP9808 i2c temperature sensor (#676) + +- **enhancements** + - **encoders** + - add atsamd21, atsamd51, atsame5x + - **pixel** + - add support for Monochrome types such as the SSD1306 display + - **rtl8720dn** + - implement ConnectModeAP + - **servo** + - add function SetAngle() to simplify API for most common use case + - **ssd1306** + - add DrawBitmap() function to complete Displayer interface + - add rotation functions for Displayer interface + - add Sleep() function for Displayer interface + - **uc8151** + - improvements to speed and also add flicker-free mode based on @antirez code example + - update to support all functions needed by tinygl and board package Displayer interface + - **wifinina** + - implement ConnectModeAP + +- **bugfixes** + - **ft6336** + - ignore bogus touch events + - **pixel** + - fix Image[Monochrome].Set for larger images + - **uc8151** + - correct DrawBitmap() also refactor SendCommand() and SendData() for clarity + - **ws2812** + - Fix typo and move initialization of neo to init() + +- **examples** + - **ws2812** + - Simplify examples/ws2812 + + 0.27.0 --- - **core** diff --git a/README.md b/README.md index bd30b16..4b5a86d 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PkgGoDev](https://pkg.go.dev/badge/tinygo.org/x/drivers)](https://pkg.go.dev/tinygo.org/x/drivers) [![Build](https://github.com/tinygo-org/drivers/actions/workflows/build.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/drivers/actions/workflows/build.yml) -This package provides a collection of 102 different hardware drivers for devices such as sensors and displays that can be used together with [TinyGo](https://tinygo.org). +This package provides a collection of over 100 different hardware drivers for devices such as sensors, displays, wireless adaptors, and actuators, that can be used together with [TinyGo](https://tinygo.org). For the complete list, please see: https://tinygo.org/docs/reference/devices/ diff --git a/version.go b/version.go index a754383..2019a9d 100644 --- a/version.go +++ b/version.go @@ -2,4 +2,4 @@ package drivers // Version returns a user-readable string showing the version of the drivers package for support purposes. // Update this value before release of new version of software. -const Version = "0.27.0" +const Version = "0.28.0" From ee3842f639a11b97a7c22b4d65eb8805f1e61518 Mon Sep 17 00:00:00 2001 From: Ron Evans Date: Mon, 1 Jul 2024 15:06:25 +0200 Subject: [PATCH 029/137] pcf8591: add ADC only implementation for I2C ADC/DAC (#690) Signed-off-by: deadprogram --- examples/pcf8591/main.go | 28 ++++++++++++++ pcf8591/pcf8591.go | 84 ++++++++++++++++++++++++++++++++++++++++ pcf8591/registers.go | 7 ++++ smoketest.sh | 1 + 4 files changed, 120 insertions(+) create mode 100644 examples/pcf8591/main.go create mode 100644 pcf8591/pcf8591.go create mode 100644 pcf8591/registers.go diff --git a/examples/pcf8591/main.go b/examples/pcf8591/main.go new file mode 100644 index 0000000..3994224 --- /dev/null +++ b/examples/pcf8591/main.go @@ -0,0 +1,28 @@ +// Connects to a pcf8591 ADC via I2C. +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/pcf8591" +) + +var ( + i2c = machine.I2C0 +) + +func main() { + i2c.Configure(machine.I2CConfig{}) + adc := pcf8591.New(i2c) + adc.Configure() + + // get "CH0" aka "machine.ADC" interface to channel 0 from ADC. + p := adc.CH0 + + for { + val := p.Get() + println(val) + time.Sleep(50 * time.Millisecond) + } +} diff --git a/pcf8591/pcf8591.go b/pcf8591/pcf8591.go new file mode 100644 index 0000000..cd57dee --- /dev/null +++ b/pcf8591/pcf8591.go @@ -0,0 +1,84 @@ +// Package pcf8591 implements a driver for the PCF8591 Analog to Digital/Digital to Analog Converter. +// +// Datasheet: https://www.nxp.com/docs/en/data-sheet/PCF8591.pdf +package pcf8591 // import "tinygo.org/x/drivers/pcf8591" + +import ( + "machine" + + "errors" + + "tinygo.org/x/drivers" +) + +// Device wraps PCF8591 ADC functions. +type Device struct { + bus drivers.I2C + Address uint16 + CH0 ADCPin + CH1 ADCPin + CH2 ADCPin + CH3 ADCPin +} + +// ADCPin is the implementation of the ADConverter interface. +type ADCPin struct { + machine.Pin + d *Device +} + +// New returns a new PCF8591 driver. Pass in a fully configured I2C bus. +func New(b drivers.I2C) *Device { + d := &Device{ + bus: b, + Address: defaultAddress, + } + + // setup all channels + d.CH0 = d.GetADC(0) + d.CH1 = d.GetADC(1) + d.CH2 = d.GetADC(2) + d.CH3 = d.GetADC(3) + + return d +} + +// Configure here just for interface compatibility. +func (d *Device) Configure() { +} + +// Read analog data from channel +func (d *Device) Read(ch int) (uint16, error) { + if ch < 0 || ch > 3 { + return 0, errors.New("invalid channel for pcf8591 Read") + } + + return d.GetADC(ch).Get(), nil +} + +// GetADC returns an ADC for a specific channel. +func (d *Device) GetADC(ch int) ADCPin { + return ADCPin{machine.Pin(ch), d} +} + +// Get the current reading for a specific ADCPin. +func (p ADCPin) Get() uint16 { + // TODO: also implement DAC + tx := make([]byte, 2) + tx[0] = byte(p.Pin) + + rx := make([]byte, 2) + + // The result from the measurement triggered by the first write, + // however, the second write is required to get the result. + // See section 8.4 "A/D Conversion" in the datasheet for more info + p.d.bus.Tx(p.d.Address, tx, rx) + p.d.bus.Tx(p.d.Address, tx, rx) + + // scale result to 16bit value like other ADCs + return uint16(rx[1] << 8) +} + +// Configure here just for interface compatibility. +func (p ADCPin) Configure() { +} diff --git a/pcf8591/registers.go b/pcf8591/registers.go new file mode 100644 index 0000000..0a660cd --- /dev/null +++ b/pcf8591/registers.go @@ -0,0 +1,7 @@ +package pcf8591 + +// PCF8591 Default Address +const defaultAddress = 0x48 + +// control bit for DAC +const PCF8591_ENABLE_DAC = 0x40 diff --git a/smoketest.sh b/smoketest.sh index 569a0d8..1237d85 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -106,6 +106,7 @@ tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/clk tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/time/ tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/timer/ tinygo build -size short -o ./build/test.hex -target=pico ./examples/qmi8658c/main.go +tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/pcf8591/ tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/ina260/main.go tinygo build -size short -o ./build/test.hex -target=nucleo-l432kc ./examples/aht20/main.go tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/sdcard/console/ From dd44f9220b58e48a49fd4c500fb0ad7f6eaff5e6 Mon Sep 17 00:00:00 2001 From: Greg Herlein Date: Wed, 14 Aug 2024 10:47:22 -0700 Subject: [PATCH 030/137] removed what seems like an accidental home directory replace in go.mod --- go.mod | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.mod b/go.mod index 366deda..0fa3076 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,6 @@ module tinygo.org/x/drivers go 1.18 -replace tinygo.org/x/drivers/mcp9808 => /home/kasterby/Documents/drivers/mcp9808 - require ( github.com/eclipse/paho.mqtt.golang v1.2.0 github.com/frankban/quicktest v1.10.2 From d688fa3f3f4030f51774271e43a3269f349aab5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=9F=E3=81=AC=E3=81=8D?= <74751456+notpop@users.noreply.github.com> Date: Mon, 19 Aug 2024 08:45:59 +0900 Subject: [PATCH 031/137] ssd1306: Add function `SetFlip` and `GetFlip` (#702) * ssd1306: Add SetRotation function --- ssd1306/registers.go | 5 +++++ ssd1306/ssd1306.go | 29 +++++++++++++++++++++-------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/ssd1306/registers.go b/ssd1306/registers.go index 63590b8..974ecee 100644 --- a/ssd1306/registers.go +++ b/ssd1306/registers.go @@ -1,5 +1,7 @@ package ssd1306 +import "tinygo.org/x/drivers" + // Registers const ( Address = 0x3D @@ -38,4 +40,7 @@ const ( EXTERNALVCC VccMode = 0x1 SWITCHCAPVCC VccMode = 0x2 + + NO_ROTATION = drivers.Rotation0 + ROTATION_180 = drivers.Rotation180 ) diff --git a/ssd1306/ssd1306.go b/ssd1306/ssd1306.go index 7aaef87..2c5d9c3 100644 --- a/ssd1306/ssd1306.go +++ b/ssd1306/ssd1306.go @@ -15,9 +15,8 @@ import ( ) var ( - errBufferSize = errors.New("invalid size buffer") - errOutOfRange = errors.New("out of screen range") - errNotImplemented = errors.New("not implemented") + errBufferSize = errors.New("invalid size buffer") + errOutOfRange = errors.New("out of screen range") ) type ResetValue [2]byte @@ -33,6 +32,7 @@ type Device struct { canReset bool resetCol ResetValue resetPage ResetValue + rotation drivers.Rotation } // Config is the configuration for the display @@ -48,6 +48,7 @@ type Config struct { // If you're using a different size, you might need to set these values manually. ResetCol ResetValue ResetPage ResetValue + Rotation drivers.Rotation } type I2CBus struct { @@ -149,8 +150,8 @@ func (d *Device) Configure(cfg Config) { } d.Command(MEMORYMODE) d.Command(0x00) - d.Command(SEGREMAP | 0x1) - d.Command(COMSCANDEC) + + d.SetRotation(cfg.Rotation) if (d.width == 128 && d.height == 64) || (d.width == 64 && d.height == 48) { // 128x64 or 64x48 d.Command(SETCOMPINS) @@ -363,13 +364,25 @@ func (d *Device) DrawBitmap(x, y int16, bitmap pixel.Image[pixel.Monochrome]) er // Rotation returns the currently configured rotation. func (d *Device) Rotation() drivers.Rotation { - return drivers.Rotation0 + return d.rotation } // SetRotation changes the rotation of the device (clock-wise). -// Would have to be implemented in software for this device. func (d *Device) SetRotation(rotation drivers.Rotation) error { - return errNotImplemented + d.rotation = rotation + switch d.rotation { + case drivers.Rotation0: + d.Command(SEGREMAP | 0x1) // Reverse horizontal mapping + d.Command(COMSCANDEC) // Reverse vertical mapping + case drivers.Rotation180: + d.Command(SEGREMAP) // Normal horizontal mapping + d.Command(COMSCANINC) // Normal vertical mapping + // nothing to do + default: + d.Command(SEGREMAP | 0x1) // Reverse horizontal mapping + d.Command(COMSCANDEC) // Reverse vertical mapping + } + return nil } // Set the sleep mode for this display. When sleeping, the panel uses a lot From 07216d3051aa2137af6b887b7109c7b99b2028f7 Mon Sep 17 00:00:00 2001 From: Greg Herlein Date: Wed, 14 Aug 2024 11:27:05 -0700 Subject: [PATCH 032/137] expanded README to discuss need to change variables in examples --- README.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b5a86d..5d4f0fd 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ go get tinygo.org/x/drivers ## How to use -Here is an example in TinyGo that uses the BMP180 digital barometer: +Here is an example in TinyGo that uses the BMP180 digital barometer. This example should work on any board that supports I2C: ```go package main @@ -53,6 +53,28 @@ func main() { } ``` +## Examples Using GPIO or SPI + +If compiling these examples directly you are likely to need to make minor changes to the defined variables to map the pins for the board you are using. For example, this block in main.go: + +```golang +var ( + spi = machine.SPI0 + csPin = machine.D5 +) +``` + +It might not be obvious, but you need to change these to match how you wired your specific board. Constants are [defined for each supported microcontroller](https://tinygo.org/docs/reference/microcontrollers/). + +For example, to change the definitions for use on a Raspberry Pi Pico using typical wiring, you might need to do this: + +```golang +var ( + spi = machine.SPI0 + csPin = machine.GP17 +) +``` + ## Contributing Your contributions are welcome! From 109cab4f3bf0b739294a065d5c51193be65a323b Mon Sep 17 00:00:00 2001 From: HattoriHanzo031 Date: Wed, 2 Oct 2024 20:07:17 +0200 Subject: [PATCH 033/137] onewire improvements --- ds18b20/ds18b20.go | 4 ++-- onewire/onewire.go | 26 +++++++++++++++++--------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/ds18b20/ds18b20.go b/ds18b20/ds18b20.go index 3553661..ccbb9fd 100644 --- a/ds18b20/ds18b20.go +++ b/ds18b20/ds18b20.go @@ -19,7 +19,7 @@ type OneWireDevice interface { Write(uint8) Read() uint8 Select([]uint8) error - Сrc8([]uint8, int) uint8 + Сrc8([]uint8) uint8 } // Device wraps a connection to an 1-Wire devices. @@ -69,7 +69,7 @@ func (d Device) ReadTemperatureRaw(romid []uint8) ([]uint8, error) { for i := 0; i < 9; i++ { spb[i] = d.owd.Read() } - if d.owd.Сrc8(spb, 8) != spb[8] { + if d.owd.Сrc8(spb) != 0 { return nil, errReadTemperature } return spb[:2:2], nil diff --git a/onewire/onewire.go b/onewire/onewire.go index 5a51cf7..ee18283 100644 --- a/onewire/onewire.go +++ b/onewire/onewire.go @@ -27,8 +27,9 @@ type Config struct{} // Errors list var ( - errNoPresence = errors.New("Error: OneWire. No devices on the bus.") - errReadAddress = errors.New("Error: OneWire. Read address error: CRC mismatch.") + errNoPresence = errors.New("Error: OneWire. No devices on the bus.") + errTooManyDevices = errors.New("Error: OneWire. Too many devices on the bus.") + errReadAddress = errors.New("Error: OneWire. Read address error: CRC mismatch.") ) // New creates a new GPIO 1-Wire connection. @@ -46,7 +47,7 @@ func (d *Device) Configure(config Config) {} func (d Device) Reset() error { d.p.Configure(machine.PinConfig{Mode: machine.PinOutput}) time.Sleep(480 * time.Microsecond) - d.p.Configure(machine.PinConfig{Mode: machine.PinInput}) + d.p.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) time.Sleep(70 * time.Microsecond) precence := d.p.Get() time.Sleep(410 * time.Microsecond) @@ -61,11 +62,11 @@ func (d Device) WriteBit(data uint8) { d.p.Configure(machine.PinConfig{Mode: machine.PinOutput}) if data&1 == 1 { // Send '1' time.Sleep(5 * time.Microsecond) - d.p.Configure(machine.PinConfig{Mode: machine.PinInput}) + d.p.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) time.Sleep(60 * time.Microsecond) } else { // Send '0' time.Sleep(60 * time.Microsecond) - d.p.Configure(machine.PinConfig{Mode: machine.PinInput}) + d.p.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) time.Sleep(5 * time.Microsecond) } } @@ -82,7 +83,7 @@ func (d Device) Write(data uint8) { func (d Device) ReadBit() (data uint8) { d.p.Configure(machine.PinConfig{Mode: machine.PinOutput}) time.Sleep(3 * time.Microsecond) - d.p.Configure(machine.PinConfig{Mode: machine.PinInput}) + d.p.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) time.Sleep(8 * time.Microsecond) if d.p.Get() { data = 1 @@ -111,7 +112,7 @@ func (d Device) ReadAddress() ([]uint8, error) { for i := 0; i < 8; i++ { romid[i] = d.Read() } - if d.Сrc8(romid, 7) != romid[7] { + if d.Сrc8(romid) != 0 { return nil, errReadAddress } return romid, nil @@ -187,15 +188,22 @@ func (d Device) Search(cmd uint8) ([][]uint8, error) { } d.WriteBit(bit) } + if d.Сrc8(lastAddress) != 0 { + continue + } + lastFork = lastZero copy(romIDs[romIndex], lastAddress) romIndex++ + if romIndex >= 32 { + return romIDs, errTooManyDevices + } } return romIDs[:romIndex:romIndex], nil } // Crc8 compute a Dallas Semiconductor 8 bit CRC. -func (d Device) Сrc8(buffer []uint8, size int) (crc uint8) { +func (_ Device) Сrc8(buffer []uint8) (crc uint8) { // Dow-CRC using polynomial X^8 + X^5 + X^4 + X^0 // Tiny 2x16 entry CRC table created by Arjen Lentz // See http://lentz.com.au/blog/calculating-crc-with-a-tiny-32-entry-lookup-table @@ -205,7 +213,7 @@ func (d Device) Сrc8(buffer []uint8, size int) (crc uint8) { 0x00, 0x9D, 0x23, 0xBE, 0x46, 0xDB, 0x65, 0xF8, 0x8C, 0x11, 0xAF, 0x32, 0xCA, 0x57, 0xE9, 0x74, } - for i := 0; i < size; i++ { + for i := 0; i < len(buffer); i++ { crc = buffer[i] ^ crc // just re-using crc as intermediate crc = crc8_table[crc&0x0f] ^ crc8_table[16+((crc>>4)&0x0f)] } From ce80e7582f5e514ccb6710213a7afcbcea77d080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Bia=C5=82o=C5=84?= Date: Wed, 23 Oct 2024 13:43:30 +0200 Subject: [PATCH 034/137] epd1in54: add Waveshare 1.54inch B/W e-Paper display (#704) epd1in54: add Waveshare 1.54inch B/W e-Paper display --- examples/waveshare-epd/epd1in54/main.go | 36 ++ smoketest.sh | 1 + waveshare-epd/epd1in54/epd1in54.go | 424 ++++++++++++++++++++++++ waveshare-epd/epd1in54/registers.go | 52 +++ 4 files changed, 513 insertions(+) create mode 100644 examples/waveshare-epd/epd1in54/main.go create mode 100644 waveshare-epd/epd1in54/epd1in54.go create mode 100644 waveshare-epd/epd1in54/registers.go diff --git a/examples/waveshare-epd/epd1in54/main.go b/examples/waveshare-epd/epd1in54/main.go new file mode 100644 index 0000000..11b34cf --- /dev/null +++ b/examples/waveshare-epd/epd1in54/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "image/color" + "machine" + + "tinygo.org/x/drivers/waveshare-epd/epd1in54" + "tinygo.org/x/tinyfont" + "tinygo.org/x/tinyfont/gophers" +) + +var ( + spi0 = machine.SPI0 + cs = machine.D10 + dc = machine.D9 + rst = machine.D6 + busy = machine.D5 + + black = color.RGBA{R: 1, G: 1, B: 1, A: 255} +) + +func main() { + display := epd1in54.New(spi0, cs, dc, rst, busy) + + display.LDirInit(epd1in54.Config{}) + display.Clear() + display.ClearBuffer() + + tinyfont.WriteLineRotated(&display, &gophers.Regular58pt, 150, 0, "A B C", black, tinyfont.ROTATION_90) + tinyfont.WriteLineRotated(&display, &gophers.Regular58pt, 100, 0, "D E F", black, tinyfont.ROTATION_90) + tinyfont.WriteLineRotated(&display, &gophers.Regular58pt, 50, 0, "G H I", black, tinyfont.ROTATION_90) + tinyfont.WriteLineRotated(&display, &gophers.Regular58pt, 0, 0, "J K L", black, tinyfont.ROTATION_90) + + display.Display() + display.Sleep() +} diff --git a/smoketest.sh b/smoketest.sh index 1237d85..211e92f 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -77,6 +77,7 @@ tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/touch/r tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/touch/resistive/pyportal_touchpaint/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/vl53l1x/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/vl6180x/main.go +tinygo build -size short -o ./build/test.hex -target=feather-nrf52840-sense ./examples/waveshare-epd/epd1in54/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd2in13/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd2in13x/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd4in2/main.go diff --git a/waveshare-epd/epd1in54/epd1in54.go b/waveshare-epd/epd1in54/epd1in54.go new file mode 100644 index 0000000..4cbda2a --- /dev/null +++ b/waveshare-epd/epd1in54/epd1in54.go @@ -0,0 +1,424 @@ +// Package epd1in54 implements a driver for Waveshare 1.54in black and white e-paper device. +// +// Derived from: +// +// https://github.com/tinygo-org/drivers/tree/master/waveshare-epd +// https://github.com/waveshare/e-Paper/blob/master/Arduino/epd1in54_V2/epd1in54_V2.cpp +// +// Datasheet: https://www.waveshare.com/w/upload/e/e5/1.54inch_e-paper_V2_Datasheet.pdf +package epd1in54 + +import ( + "image/color" + "machine" + "time" +) + +type Config struct { + Width int16 + Height int16 + LogicalWidth int16 + Rotation Rotation +} + +type Device struct { + bus machine.SPI + cs machine.Pin + dc machine.Pin + rst machine.Pin + busy machine.Pin + + buffer []uint8 + rotation Rotation +} + +type Rotation uint8 + +var fullRefresh = [159]uint8{ + 0x80, 0x48, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x48, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x80, 0x48, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x48, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x8, 0x1, 0x0, 0x8, 0x1, 0x0, 0x2, + 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, + 0x22, 0x17, 0x41, 0x0, 0x32, 0x20, +} + +var partialRefresh = [159]uint8{ + 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, + 0x02, 0x17, 0x41, 0xB0, 0x32, 0x28, +} + +// New returns a new epd1in54 driver. Pass in a fully configured SPI bus. +func New(bus machine.SPI, csPin, dcPin, rstPin, busyPin machine.Pin) Device { + return Device{ + buffer: make([]uint8, (uint32(Width)*uint32(Height))/8), + bus: bus, + cs: csPin, + dc: dcPin, + rst: rstPin, + busy: busyPin, + } +} + +func (d *Device) LDirInit(cfg Config) { + d.cs.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.rst.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.dc.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.busy.Configure(machine.PinConfig{Mode: machine.PinInput}) + + d.bus.Configure(machine.SPIConfig{ + Frequency: 2000000, + Mode: 0, + LSBFirst: false, + }) + + d.Reset() + d.WaitUntilIdle() + + d.SendCommand(0x12) + d.WaitUntilIdle() + + d.SendCommand(0x01) + d.SendData(0xC7) + d.SendData(0x00) + d.SendData(0x00) + + d.SendCommand(0x11) + d.SendData(0x03) + + d.SendCommand(0x44) + /* x point must be the multiple of 8 or the last 3 bits will be ignored */ + d.SendData((0 >> 3) & 0xFF) + d.SendData((199 >> 3) & 0xFF) + + d.SendCommand(0x45) + d.SendData(0 & 0xFF) + d.SendData((0 >> 8) & 0xFF) + d.SendData(199 & 0xFF) + d.SendData((199 >> 8) & 0xFF) + + d.SendCommand(0x3C) + d.SendData(0x01) + + d.SendCommand(0x18) + d.SendData(0x80) + + d.SendCommand(0x22) + d.SendData(0xB1) + + d.SendCommand(0x20) + + d.SendCommand(0x4E) + d.SendData(0x00) + + d.SendCommand(0x4F) + d.SendData(0xC7) + d.SendData(0x00) + + d.WaitUntilIdle() + d.setLUT(fullRefresh) +} + +func (d *Device) HDirInit(cfg Config) { + d.cs.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.rst.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.dc.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.busy.Configure(machine.PinConfig{Mode: machine.PinInput}) + + d.bus.Configure(machine.SPIConfig{ + Frequency: 2000000, + Mode: 0, + LSBFirst: false, + }) + + d.Reset() + d.WaitUntilIdle() + + d.SendCommand(0x12) + d.WaitUntilIdle() + + d.SendCommand(0x01) + d.SendData(0xC7) + d.SendData(0x00) + d.SendData(0x01) + + d.SendCommand(0x11) + d.SendData(0x01) + + d.SendCommand(0x44) + d.SendData(0x00) + d.SendData(0x18) + + d.SendCommand(0x45) + d.SendData(0xC7) + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x00) + + d.SendCommand(0x3C) + d.SendData(0x01) + + d.SendCommand(0x18) + d.SendData(0x80) + + d.SendCommand(0x22) + d.SendData(0xB1) + + d.SendCommand(0x20) + + d.SendCommand(0x4E) + d.SendData(0x00) + + d.SendCommand(0x4F) + d.SendData(0xC7) + d.SendData(0x00) + + d.WaitUntilIdle() + d.setLUT(fullRefresh) +} + +func (d *Device) setLUT(lut [159]uint8) { + d.SendCommand(0x32) + for i := 0; i < 153; i++ { + d.SendData(lut[i]) + } + d.WaitUntilIdle() + + d.SendCommand(0x3F) + d.SendData(lut[153]) + + d.SendCommand(0x03) + d.SendData(lut[154]) + + d.SendCommand(0x04) + d.SendData(lut[155]) + d.SendData(lut[156]) + d.SendData(lut[157]) + + d.SendCommand(0x2C) + d.SendData(lut[158]) +} + +// Reset resets the display. +func (d *Device) Reset() { + d.rst.High() + time.Sleep(20 * time.Millisecond) + d.rst.Low() + time.Sleep(5 * time.Millisecond) + d.rst.High() + time.Sleep(20 * time.Millisecond) +} + +// SendCommand sends a command to the display +func (d *Device) SendCommand(command uint8) { + d.sendDataCommand(true, command) +} + +// SendData sends a data byte to the display +func (d *Device) SendData(data uint8) { + d.sendDataCommand(false, data) +} + +// sendDataCommand sends image data or a command to the screen +func (d *Device) sendDataCommand(isCommand bool, data uint8) { + if isCommand { + d.dc.Low() + } else { + d.dc.High() + } + d.cs.Low() + d.bus.Transfer(data) + d.cs.High() +} + +// SetPixel modifies the internal buffer in a single pixel. +// The display have 2 colors: black and white +// We use RGBA(0,0,0, 255) as white (transparent) +// Anything else as black +func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { + x, y = d.xy(x, y) + if x < 0 || x >= Width || y < 0 || y >= Height { + return + } + byteIndex := (uint32(x) + uint32(y)*uint32(Width)) / 8 + if c.R == 0 && c.G == 0 && c.B == 0 { // TRANSPARENT / WHITE + d.buffer[byteIndex] |= 0x80 >> uint8(x%8) + } else { // WHITE / EMPTY + d.buffer[byteIndex] &^= 0x80 >> uint8(x%8) + } +} + +func (d *Device) DisplayImage(image []uint8) { + var w, h int + if Width%8 == 0 { + w = int(Width / 8) + } else { + w = int(Width/8 + 1) + } + h = int(Height) + + d.SendCommand(0x24) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + d.SendData(image[i+j*w]) + } + } + + d.SendCommand(0x26) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + d.SendData(image[i+j*w]) + } + } + + d.displayFrame() +} + +func (d *Device) Display() error { + var w, h int + if Width%8 == 0 { + w = int(Width / 8) + } else { + w = int(Width/8 + 1) + } + h = int(Height) + + d.SendCommand(0x24) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + x := i + j*w + d.SendData(d.buffer[x]) + } + } + + d.SendCommand(0x26) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + x := i + j*w + d.SendData(d.buffer[x]) + } + } + + d.displayFrame() + + return nil +} + +func (d *Device) displayFrame() { + d.SendCommand(0x22) + d.SendData(0xC7) + d.SendCommand(0x20) + d.WaitUntilIdle() +} + +func (d *Device) Clear() { + var w, h int + if Width%8 == 0 { + w = int(Width / 8) + } else { + w = int(Width/8 + 1) + } + h = int(Height) + + d.SendCommand(0x24) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + d.SendData(0xff) + } + } + + d.SendCommand(0x26) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + d.SendData(0xff) + } + } + + d.displayFrame() +} + +// WaitUntilIdle waits until the display is ready +func (d *Device) WaitUntilIdle() { + for d.busy.Get() { + time.Sleep(100 * time.Millisecond) + } + time.Sleep(200 * time.Millisecond) +} + +// IsBusy returns the busy status of the display +func (d *Device) IsBusy() bool { + return d.busy.Get() +} + +// ClearBuffer sets the buffer to 0xFF (white) +func (d *Device) ClearBuffer() { + for i := 0; i < len(d.buffer); i++ { + d.buffer[i] = 0xFF + } +} + +// Size returns the current size of the display. +func (d *Device) Size() (w, h int16) { + if d.rotation == ROTATION_90 || d.rotation == ROTATION_270 { + return Height, Width + } + return Width, Height +} + +// SetRotation changes the rotation (clock-wise) of the device +func (d *Device) SetRotation(rotation Rotation) { + d.rotation = rotation +} + +// xy chages the coordinates according to the rotation +func (d *Device) xy(x, y int16) (int16, int16) { + switch d.rotation { + case NO_ROTATION: + return x, y + case ROTATION_90: + return Width - y - 1, x + case ROTATION_180: + return Width - x - 1, Height - y - 1 + case ROTATION_270: + return y, Height - x - 1 + } + return x, y +} + +func (d *Device) Sleep() { + d.SendCommand(0x10) + d.SendData(0x01) + time.Sleep(200 * time.Millisecond) + + d.rst.Low() +} diff --git a/waveshare-epd/epd1in54/registers.go b/waveshare-epd/epd1in54/registers.go new file mode 100644 index 0000000..ba09c8d --- /dev/null +++ b/waveshare-epd/epd1in54/registers.go @@ -0,0 +1,52 @@ +package epd1in54 + +// Derived from https://github.com/waveshare/e-Paper/blob/master/Arduino/epd4in2/epd4in2.h + +const ( + Width = 200 + Height = 200 + + PANEL_SETTING = 0x00 + POWER_SETTING = 0x01 + POWER_OFF = 0x02 + POWER_OFF_SEQUENCE_SETTING = 0x03 + POWER_ON = 0x04 + POWER_ON_MEASURE = 0x05 + BOOSTER_SOFT_START = 0x06 + DEEP_SLEEP = 0x07 + DATA_START_TRANSMISSION_1 = 0x10 + DATA_STOP = 0x11 + DISPLAY_REFRESH = 0x12 + DATA_START_TRANSMISSION_2 = 0x13 + LUT_FOR_VCOM = 0x20 + LUT_WHITE_TO_WHITE = 0x21 + LUT_BLACK_TO_WHITE = 0x22 + LUT_WHITE_TO_BLACK = 0x23 + LUT_BLACK_TO_BLACK = 0x24 + PLL_CONTROL = 0x30 + TEMPERATURE_SENSOR_COMMAND = 0x40 + TEMPERATURE_SENSOR_SELECTION = 0x41 + TEMPERATURE_SENSOR_WRITE = 0x42 + TEMPERATURE_SENSOR_READ = 0x43 + VCOM_AND_DATA_INTERVAL_SETTING = 0x50 + LOW_POWER_DETECTION = 0x51 + TCON_SETTING = 0x60 + RESOLUTION_SETTING = 0x61 + GSST_SETTING = 0x65 + GET_STATUS = 0x71 + AUTO_MEASUREMENT_VCOM = 0x80 + READ_VCOM_VALUE = 0x81 + VCM_DC_SETTING = 0x82 + PARTIAL_WINDOW = 0x90 + PARTIAL_IN = 0x91 + PARTIAL_OUT = 0x92 + PROGRAM_MODE = 0xA0 + ACTIVE_PROGRAMMING = 0xA1 + READ_OTP = 0xA2 + POWER_SAVING = 0xE3 + + NO_ROTATION Rotation = 0 + ROTATION_90 Rotation = 1 + ROTATION_180 Rotation = 2 + ROTATION_270 Rotation = 3 +) From 1c2b802f47d47e1c6c12dd47ad8380f247c207c9 Mon Sep 17 00:00:00 2001 From: PWND0U <42665365+PWND0U@users.noreply.github.com> Date: Wed, 23 Oct 2024 21:56:41 +0800 Subject: [PATCH 035/137] servo: Add function `SetAngleWithMicroseconds` (#695) servo: Add function `SetAngleWithMicroseconds`,Adjust the angle by customizing the control pulse width --- servo/servo.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/servo/servo.go b/servo/servo.go index 73ef8e0..e073aa1 100644 --- a/servo/servo.go +++ b/servo/servo.go @@ -102,3 +102,16 @@ func (s Servo) SetAngle(angle int) error { return nil } + +// SetAngleWithMicroseconds sets the angle of the servo in degrees. The angle should be between +// 0 and 180, where 0 is the minimum angle and 180 is the maximum angle. +// The high duration can be customized +// 0° is lowMicroseconds(us), 180° is highMicroseconds(us) +func (s Servo) SetAngleWithMicroseconds(angle int, lowMicroseconds, highMicroseconds int) error { + if angle < 0 || angle > 180 { + return ErrInvalidAngle + } + microseconds := lowMicroseconds + (highMicroseconds-lowMicroseconds)*angle/180 + s.SetMicroseconds(int16(microseconds)) + return nil +} From f308f8fce0f42decff3067c7895b785b08e46a0a Mon Sep 17 00:00:00 2001 From: Daniel Esteban Date: Mon, 14 Oct 2024 09:33:19 +0200 Subject: [PATCH 036/137] Add sponsor button to key repositories --- .github/FUNDING.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..016f657 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +# These are supported funding model platforms +open_collective: tinygo From 6d431e0726f690a8dc80ef53cbda5c60ce715834 Mon Sep 17 00:00:00 2001 From: Warren Guy <5602790+warrenguy@users.noreply.github.com> Date: Thu, 24 Oct 2024 06:45:33 +0100 Subject: [PATCH 037/137] Add I2C INA219 driver (#705) INA219: add INA219 driver --- examples/ina219/main.go | 29 +++++ ina219/config.go | 183 +++++++++++++++++++++++++++++++ ina219/errors.go | 13 +++ ina219/ina219.go | 202 ++++++++++++++++++++++++++++++++++ ina219/ina219_test.go | 232 ++++++++++++++++++++++++++++++++++++++++ ina219/registers.go | 13 +++ smoketest.sh | 1 + 7 files changed, 673 insertions(+) create mode 100644 examples/ina219/main.go create mode 100644 ina219/config.go create mode 100644 ina219/errors.go create mode 100644 ina219/ina219.go create mode 100644 ina219/ina219_test.go create mode 100644 ina219/registers.go diff --git a/examples/ina219/main.go b/examples/ina219/main.go new file mode 100644 index 0000000..50fcba2 --- /dev/null +++ b/examples/ina219/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/ina219" +) + +func main() { + machine.I2C0.Configure(machine.I2CConfig{}) + + dev := ina219.New(machine.I2C0) + dev.Configure() + + for { + busVoltage, shuntVoltage, current, power, err := dev.Measurements() + if err != nil { + println("Error reading measurements", err) + } + + println("Bus Voltage:", busVoltage, "V") + println("Shunt Voltage:", shuntVoltage/100, "mV") + println("Current:", current, "mA") + println("Power:", power, "mW") + + time.Sleep(10 * time.Millisecond) + } +} diff --git a/ina219/config.go b/ina219/config.go new file mode 100644 index 0000000..4c02179 --- /dev/null +++ b/ina219/config.go @@ -0,0 +1,183 @@ +package ina219 + +type Config struct { + // BusVoltageRange sets the bus voltage range. + BusVoltageRange BusVoltageRange + + // PGA sets the programmable gain amplifier. + PGA PGA + + // BusADC sets the bus ADC resolution. + BusADC BusADC + + // ShuntADC sets the shunt ADC resolution. + ShuntADC ShuntADC + + // Mode sets the operating mode. + Mode Mode + + // Calibration sets the calibration value for the expected + // voltage and current values. + Calibration Calibration + + // 1000 / uA per bit + CurrentDivider float32 + + // 1mW per bit + PowerMultiplier float32 +} + +// RegisterValue returns the register value of the configuration. +func (c *Config) RegisterValue() uint16 { + return c.BusVoltageRange.RegisterValue() | + c.PGA.RegisterValue() | + c.BusADC.RegisterValue() | + c.ShuntADC.RegisterValue() | + c.Mode.RegisterValue() +} + +// Generate a new configuration from a register value. +func NewConfig(config int16, calibration int16) Config { + return Config{ + BusVoltageRange: BusVoltageRange(config >> 13 & 0x1), + PGA: PGA(config >> 11 & 0x3), + BusADC: BusADC(config >> 7 & 0xF), + ShuntADC: ShuntADC(config >> 3 & 0xF), + Mode: Mode(config & 0x7), + Calibration: Calibration(calibration), + } +} + +// Configurations from +// https://github.com/adafruit/Adafruit_INA219/blob/master/Adafruit_INA219.cpp +var ( + // Config32V2A is a configuration for a 32V 2A range. + Config32V2A = Config{ + BusVoltageRange: Range32V, + PGA: PGA8, + BusADC: ADC12, + ShuntADC: SADC12, + Mode: ModeContShuntBus, + Calibration: Calibration16V400mA, + CurrentDivider: 10.0, + PowerMultiplier: 2.0, + } + + // Config32V1A is a configuration for a 32V 1A range. + Config32V1A = Config{ + BusVoltageRange: Range32V, + PGA: PGA8, + BusADC: ADC12, + ShuntADC: SADC12, + Mode: ModeContShuntBus, + Calibration: Calibration32V1A, + CurrentDivider: 25.0, + PowerMultiplier: 0.8, + } + + // Config16V400mA is a configuration for a 16V 400mA range. + Config16V400mA = Config{ + BusVoltageRange: Range16V, + PGA: PGA1, + BusADC: ADC12, + ShuntADC: SADC12, + Mode: ModeContShuntBus, + Calibration: Calibration16V400mA, + CurrentDivider: 20.0, + PowerMultiplier: 1.0, + } +) + +// BusVoltageRange is the bus voltage range. +type BusVoltageRange int8 + +const ( + Range16V BusVoltageRange = 0 // 0-16V + Range32V BusVoltageRange = 1 // 0-32V +) + +func (r BusVoltageRange) RegisterValue() uint16 { + return uint16(r) << 13 +} + +// PGA is the programmable gain amplifier. +type PGA int8 + +const ( + PGA1 PGA = 0 // 40mV + PGA2 PGA = 1 // 80mV + PGA4 PGA = 2 // 160mV + PGA8 PGA = 3 // 320mV +) + +func (p PGA) RegisterValue() uint16 { + return uint16(p) << 11 +} + +// BusADC is the bus ADC resolution. +type BusADC int8 + +const ( + ADC9 BusADC = 0 // 9-bit + ADC10 BusADC = 1 // 10-bit + ADC11 BusADC = 2 // 11-bit + ADC12 BusADC = 3 // 12-bit +) + +func (b BusADC) RegisterValue() uint16 { + return uint16(b) << 7 +} + +// ShuntADC is the shunt ADC resolution. +type ShuntADC int8 + +const ( + SADC9 ShuntADC = 0 // 9-bit + SADC10 ShuntADC = 1 // 10-bit + SADC11 ShuntADC = 2 // 11-bit + SADC12 ShuntADC = 3 // 12-bit +) + +func (s ShuntADC) RegisterValue() uint16 { + return uint16(s) << 3 +} + +// Mode is the operating mode. +type Mode int8 + +const ( + ModePowerDown Mode = 0 // power-down + ModeTrigShunt Mode = 1 // triggered shunt voltage + ModeTrigBus Mode = 2 // triggered bus voltage + ModeTrigShuntBus Mode = 3 // triggered shunt and bus voltage + ModeADCOff Mode = 4 // ADC off + ModeContShunt Mode = 5 // continuous shunt voltage + ModeContBus Mode = 6 // continuous bus voltage + ModeContShuntBus Mode = 7 // continuous shunt and bus voltage +) + +// ModeTriggered is a mask for triggered modes. +const ModeTriggeredMask Mode = 0x4 + +// ModeTriggered returns true if the mode is a triggered mode. +func ModeTriggered(m Mode) bool { + return m != ModePowerDown && m&ModeTriggeredMask == 0 +} + +func (m Mode) RegisterValue() uint16 { + return uint16(m) +} + +// Calibration is the calibration register for the INA219. Values from: +// https://github.com/adafruit/Adafruit_INA219/blob/master/Adafruit_INA219.cpp +type Calibration uint16 + +const ( + Calibration32V2A Calibration = 4096 + Calibration32V1A Calibration = 10240 + Calibration16V400mA Calibration = 8192 +) + +func (c Calibration) RegisterValue() uint16 { + return uint16(c) +} diff --git a/ina219/errors.go b/ina219/errors.go new file mode 100644 index 0000000..fe078fc --- /dev/null +++ b/ina219/errors.go @@ -0,0 +1,13 @@ +package ina219 + +type ErrOverflow struct{} + +func (e ErrOverflow) Error() string { return "overflow" } + +type ErrNotReady struct{} + +func (e ErrNotReady) Error() string { return "not ready" } + +type ErrConfigMismatch struct{} + +func (e ErrConfigMismatch) Error() string { return "config mismatch" } diff --git a/ina219/ina219.go b/ina219/ina219.go new file mode 100644 index 0000000..0f7c299 --- /dev/null +++ b/ina219/ina219.go @@ -0,0 +1,202 @@ +package ina219 + +import ( + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" +) + +// An INA219 device. +type Device struct { + bus drivers.I2C + Address uint16 + config Config +} + +// Create a new INA219 device with the default configuration +// and the given I2C bus at the default address. +// +// Set Address after New to change the address. +// +// Call Configure after New to write the configuration to the +// device. If you don't call Configure, the device may have a +// different configuration and the power divider and current +// multiplier are probably wrong. +func New(bus drivers.I2C) Device { + return Device{ + bus: bus, + Address: Address, + config: Config32V2A, + } +} + +// Set the configuration for the device. This only changes the +// configuration in memory, not on the device. Call Configure +// to write the configuration to the device. +func (d *Device) SetConfig(config Config) { + d.config = config +} + +// Write the current configuration to the device. +func (d *Device) Configure() (err error) { + if err = d.WriteRegister( + RegConfig, + d.config.RegisterValue(), + ); err != nil { + return + } + + if err = d.WriteRegister( + RegCalibration, + d.config.Calibration.RegisterValue(), + ); err != nil { + return + } + + var readConfig Config + // make sure the configuration is read back correctly + if readConfig, err = d.ReadConfig(); err != nil { + return + } else if readConfig.RegisterValue() != d.config.RegisterValue() { + err = ErrConfigMismatch{} + } else if readConfig.Calibration.RegisterValue() != d.config.Calibration.RegisterValue() { + err = ErrConfigMismatch{} + } + + return +} + +// Trigger a conversion. This is only necessary if the device is in +// trigger mode. In continuous mode (the default), the device will +// automatically trigger conversions and this has no effect. See +// config.go. +// +// Triggering a conversion or reading the "power" register resets +// the conversion ready bit. +func (d *Device) Trigger() (err error) { + // Only trigger if the mode is one of the triggered modes. + if ModeTriggered(d.config.Mode) { + err = d.WriteRegister(RegConfig, d.config.RegisterValue()) + } + return +} + +// Measurements reads the bus voltage, shunt voltage, current, and power +// from the device. +func (d *Device) Measurements() ( + busVoltage int16, + shuntVoltage int16, + current float32, + power float32, + err error, +) { + // Attempt to read bus voltage first, so we can check for overflow + // or conversion not ready. + if busVoltage, err = d.BusVoltage(); err != nil { + return + } + + // Read the rest of the values, reading Power last, which resets + // the conversion ready bit (relevant for triggered modes). + if shuntVoltage, err = d.ShuntVoltage(); err != nil { + return + } + + if current, err = d.Current(); err != nil { + return + } + + if power, err = d.Power(); err != nil { + return + } + + return +} + +// BusVoltage reads the "bus" voltage in millivolts. +// +// It returns an error if the value is invalid due to overflow +// or if the conversion is not ready yet. In a continuous mode +// there should always be a measurement available after the +// device is ready. See above notes on Trigger. +func (d *Device) BusVoltage() (voltage int16, err error) { + val, err := d.ReadRegister(RegBusVoltage) + if err != nil { + return + } + + // The overflow bit is set, so the values are invalid. + if val&(1<<0) != 0 { + err = ErrOverflow{} + return + } + + // The conversion is not ready yet. + if ModeTriggered(d.config.Mode) && val&(1<<1) != 0 { + err = ErrNotReady{} + return + } + + voltage = (int16(val) >> 3) * 4 + return +} + +// ShuntVoltage reads the "shunt" voltage in 100ths of a millivolt. +func (d *Device) ShuntVoltage() (voltage int16, err error) { + return d.ReadRegister(RegShuntVoltage) +} + +// Current reads the current in milliamps. +func (d *Device) Current() (current float32, err error) { + val, err := d.ReadRegister(RegCurrent) + if err != nil { + return + } + + current = float32(val) / d.config.CurrentDivider + return +} + +// Power reads the power in milliwatts. +func (d *Device) Power() (power float32, err error) { + val, err := d.ReadRegister(RegPower) + if err != nil { + return + } + + power = float32(val) * d.config.PowerMultiplier + return +} + +// Read the configuration from the device. +func (d *Device) ReadConfig() (config Config, err error) { + var cfg, cal int16 + if cfg, err = d.ReadRegister(RegConfig); err != nil { + return + } + + if cal, err = d.ReadRegister(RegCalibration); err != nil { + return + } + + config = NewConfig(cfg, cal) + return +} + +// Read a register from the device. +func (d *Device) ReadRegister(reg uint8) (val int16, err error) { + buf := make([]byte, 2) + + err = legacy.ReadRegister(d.bus, uint8(d.Address), reg, buf) + if err != nil { + return + } + + val = int16(buf[0])<<8 | int16(buf[1]&0xff) + return +} + +// Write to a register on the device. +func (d *Device) WriteRegister(reg uint8, val uint16) error { + buf := []byte{byte(val >> 8), byte(val & 0xff)} + return legacy.WriteRegister(d.bus, uint8(d.Address), reg, buf) +} diff --git a/ina219/ina219_test.go b/ina219/ina219_test.go new file mode 100644 index 0000000..f5a12ab --- /dev/null +++ b/ina219/ina219_test.go @@ -0,0 +1,232 @@ +package ina219 + +import ( + "fmt" + "testing" + + qt "github.com/frankban/quicktest" + "tinygo.org/x/drivers/tester" +) + +func TestDefaultAddress(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + dev := New(bus) + c.Assert(dev.Address, qt.Equals, uint16(Address)) +} + +func TestBusVoltage(t *testing.T) { + t.Run("valid", func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegBusVoltage: (4200 << 3) / 4, // 4.2V + } + bus.AddDevice(fake) + + dev := New(bus) + voltage, err := dev.BusVoltage() + c.Assert(err, qt.IsNil) + c.Assert(voltage, qt.Equals, int16(4200)) + }) + + t.Run("overflow", func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegBusVoltage: (1 >> 0), // overflow + } + bus.AddDevice(fake) + + dev := New(bus) + _, err := dev.BusVoltage() + c.Assert(err, qt.Not(qt.IsNil)) + c.Assert(err, qt.ErrorMatches, ErrOverflow{}.Error()) + }) + + t.Run("not ready", func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegBusVoltage: ((4200 << 3) / 4) | (1 << 1), // not ready + } + bus.AddDevice(fake) + + dev := New(bus) + dev.config.Mode = ModeTrigBus + _, err := dev.BusVoltage() + c.Assert(err, qt.Not(qt.IsNil)) + c.Assert(err, qt.ErrorMatches, ErrNotReady{}.Error()) + }) +} + +func TestShuntVoltage(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegShuntVoltage: 0x1234, + } + bus.AddDevice(fake) + + dev := New(bus) + voltage, err := dev.ShuntVoltage() + c.Assert(err, qt.IsNil) + c.Assert(voltage, qt.Equals, int16(0x1234)) +} + +func TestCurrent(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegCurrent: 420 * 6.9, // 420mA + } + bus.AddDevice(fake) + + dev := New(bus) + dev.config.CurrentDivider = 6.9 + current, err := dev.Current() + c.Assert(err, qt.IsNil) + c.Assert(current, qt.Equals, float32(420)) +} + +func TestPower(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegPower: 420 / 0.8, // 420mW + } + bus.AddDevice(fake) + + dev := New(bus) + dev.config.PowerMultiplier = 0.8 + power, err := dev.Power() + c.Assert(err, qt.IsNil) + c.Assert(power, qt.Equals, float32(420)) +} + +func TestReadConfig(t *testing.T) { + // use the default configurations + for _, tc := range []Config{ + Config16V400mA, + Config32V2A, + Config32V1A, + } { + n := fmt.Sprintf("%x/%x", tc.RegisterValue(), tc.Calibration.RegisterValue()) + t.Run(n, func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegConfig: tc.RegisterValue(), + RegCalibration: tc.Calibration.RegisterValue(), + } + bus.AddDevice(fake) + + dev := New(bus) + config, err := dev.ReadConfig() + c.Assert(err, qt.IsNil) + c.Assert(config.BusADC, qt.Equals, tc.BusADC) + c.Assert(config.BusVoltageRange, qt.Equals, tc.BusVoltageRange) + c.Assert(config.Calibration, qt.Equals, tc.Calibration) + c.Assert(config.Mode, qt.Equals, tc.Mode) + c.Assert(config.PGA, qt.Equals, tc.PGA) + c.Assert(config.ShuntADC, qt.Equals, tc.ShuntADC) + }) + } +} + +func TestWriteConfig(t *testing.T) { + // use the default configurations + for _, tc := range []Config{ + Config16V400mA, + Config32V2A, + Config32V1A, + } { + n := fmt.Sprintf("%x/%x", tc.RegisterValue(), tc.Calibration.RegisterValue()) + t.Run(n, func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + bus.AddDevice(fake) + fake.Registers = map[uint8]uint16{ + RegConfig: 0, + RegCalibration: 0, + } + + dev := New(bus) + dev.config = tc + err := dev.Configure() + c.Assert(err, qt.IsNil) + c.Assert(fake.Registers[RegConfig], qt.Equals, tc.RegisterValue()) + c.Assert(fake.Registers[RegCalibration], qt.Equals, tc.Calibration.RegisterValue()) + }) + } +} + +func TestSetConfig(t *testing.T) { + for _, tc := range []Config{ + Config16V400mA, + Config32V2A, + Config32V1A, + } { + n := fmt.Sprintf("%x/%x", tc.RegisterValue(), tc.Calibration.RegisterValue()) + t.Run(n, func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + dev := New(bus) + dev.SetConfig(tc) + c.Assert(dev.config, qt.Equals, tc) + }) + } +} + +func TestTrigger(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + bus.AddDevice(fake) + fake.Registers = map[uint8]uint16{ + RegConfig: Config32V2A.RegisterValue(), + } + + dev := New(bus) + dev.config = Config32V2A + dev.config.Mode = ModeTrigBus + err := dev.Trigger() + c.Assert(err, qt.IsNil) + c.Assert(fake.Registers[RegConfig], qt.Equals, dev.config.RegisterValue()) +} + +func TestMeasurements(t *testing.T) { + bvVal := int16(4200) + svVal := int16(1234) + iVal := float32(420) + pVal := float32(420) + + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + bus.AddDevice(fake) + fake.Registers = map[uint8]uint16{ + RegBusVoltage: uint16(((4200 << 3) / 4) | (1 << 1)), + RegShuntVoltage: uint16(svVal), + RegCurrent: uint16(iVal * Config16V400mA.CurrentDivider), + RegPower: uint16(pVal / Config16V400mA.PowerMultiplier), + } + + dev := New(bus) + dev.config = Config16V400mA + bv, sv, i, p, err := dev.Measurements() + c.Assert(err, qt.IsNil) + c.Assert(bv, qt.Equals, bvVal) + c.Assert(sv, qt.Equals, svVal) + c.Assert(i, qt.Equals, iVal) + c.Assert(p, qt.Equals, pVal) + +} diff --git a/ina219/registers.go b/ina219/registers.go new file mode 100644 index 0000000..f1bae3a --- /dev/null +++ b/ina219/registers.go @@ -0,0 +1,13 @@ +package ina219 + +// The default I2C address for this device. +const Address = 0x40 + +const ( + RegConfig uint8 = 0x0 + RegShuntVoltage uint8 = 0x1 + RegBusVoltage uint8 = 0x2 + RegPower uint8 = 0x3 + RegCurrent uint8 = 0x4 + RegCalibration uint8 = 0x5 +) diff --git a/smoketest.sh b/smoketest.sh index 211e92f..b09c063 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -109,6 +109,7 @@ tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/tim tinygo build -size short -o ./build/test.hex -target=pico ./examples/qmi8658c/main.go tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/pcf8591/ tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/ina260/main.go +tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/ina219/main.go tinygo build -size short -o ./build/test.hex -target=nucleo-l432kc ./examples/aht20/main.go tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/sdcard/console/ tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/i2csoft/adt7410/ From 30f540c29ff233e335d92ee49ada71cf3dbcda08 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sat, 19 Oct 2024 11:32:22 +0200 Subject: [PATCH 038/137] touch: add capacitive touch sensing on normal GPIO pins Tested on the following chips/boards: * RP2040 (Raspberry Pi Pico) * ATSAMD21 (Adafruit PyBadge) * NRF52840 (PCA10056 developer board) * ESP8266 (NodeMCU) * ATmega328p (Arduino Uno) * ESP32C3 (WaveShare ESP-C3-32S-Kit) * FE310 (SiFive HiFive1 rev B) The sensitivity threshold in the example may need to be adjusted per board though, the default value of 100 typically recognizes when a cable is being touched but the RP2040 for example is capable of doing much more precise measurements if the power supply is sufficiently noise-free. --- examples/touch/capacitive/main.go | 69 +++++++ smoketest.sh | 1 + touch/capacitive/gpio.go | 313 ++++++++++++++++++++++++++++++ 3 files changed, 383 insertions(+) create mode 100644 examples/touch/capacitive/main.go create mode 100644 touch/capacitive/gpio.go diff --git a/examples/touch/capacitive/main.go b/examples/touch/capacitive/main.go new file mode 100644 index 0000000..fc3f926 --- /dev/null +++ b/examples/touch/capacitive/main.go @@ -0,0 +1,69 @@ +// Capacitive touch sensing example. +// +// This capacitive touch sensor works by charging a normal GPIO pin, then slowly +// discharging it through a 1MΩ resistor and seeing how long it takes to go from +// high to low. +// +// Use as follows: +// - Change touchPin below as needed. +// - Connect this pin to some metal surface, like a piece of aluminimum foil. +// Make sure this surface is covered (using paper, Scotch tape, etc). +// - Also connect this same pin to ground through a 1MΩ resistor. +// +// This sensor is very sensitive to noise on the power source, so you should +// probably try to limit it by running from a battery for example. Especially +// phone chargers can produce a lot of noise. +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/touch/capacitive" +) + +const touchPin = machine.GP16 // Raspberry Pi Pico + +func main() { + time.Sleep(time.Second * 2) + println("start") + + led := machine.LED + led.Configure(machine.PinConfig{Mode: machine.PinOutput}) + led.Low() + + // Configure the array of GPIO pins used for capacitive touch sensing. + // We're using only one pin. + array := capacitive.NewArray([]machine.Pin{touchPin}) + + // Use a dynamic threshold, meaning the GPIO pin is automatically calibrated + // and re-calibrated to adjust for varying environments (e.g. changing + // humidity). + array.SetDynamicThreshold(100) + + wasTouching := false + for i := uint32(0); ; i++ { + // Update the GPIO pin. This must be called very often. + array.Update() + touching := array.Touching(0) + + // Indicate whether the pin is touched via the LED. + led.Set(touching) + + // Print something when the touch state changed. + if wasTouching != touching { + wasTouching = touching + if touching { + println(" touch!") + } else { + println(" release!") + } + } + + // Print the current value, as a debugging aid. It's not really meant to + // be used directly. + if i%128 == 32 { + println("touch value:", array.RawValue(0)) + } + } +} diff --git a/smoketest.sh b/smoketest.sh index b09c063..c89ba04 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -73,6 +73,7 @@ tinygo build -size short -o ./build/test.hex -target=microbit ./examples/st7789/ tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/thermistor/main.go tinygo build -size short -o ./build/test.hex -target=circuitplay-bluefruit ./examples/tone tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/tm1637/main.go +tinygo build -size short -o ./build/test.hex -target=pico ./examples/touch/capacitive tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/touch/resistive/fourwire/main.go tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/touch/resistive/pyportal_touchpaint/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/vl53l1x/main.go diff --git a/touch/capacitive/gpio.go b/touch/capacitive/gpio.go new file mode 100644 index 0000000..c98d353 --- /dev/null +++ b/touch/capacitive/gpio.go @@ -0,0 +1,313 @@ +package capacitive + +import ( + "machine" + "runtime/interrupt" + "time" +) + +const ( + // How often to measure. + // The Update function will wait until this amount of time has passed. + measurementFrequency = 200 + minTimeBetweenMeasurements = time.Second / measurementFrequency + + // How much to multiply values before averaging. A value higher than 1 will + // help to avoid integer rounding errors and may improve accuracy slightly. + oversampling = 8 + + // How many samples to use for the moving average. + movingAverageWindow = 16 + + // After how many samples should the touch sensor be recalibrated? + // This should be a power of two (for efficient division) and be a multiple + // of movingAverageWindow. Ideally it should cause a recalibration every 5s + // or so. + recalibrationSamples = 1024 +) + +type Array struct { + // Time when the last update finished. This is used to make sure we call + // Update() the expected number of times per second. + lastUpdate time.Time + + // List of pins to measure each time. + pins []machine.Pin + + // Raw values (non-smoothed) from the last read. + values []uint16 + + hasFirstMeasurement bool + + // Static threshold. Zero if using a dynamic threshold. + staticThreshold uint16 + + // How long to measure. + measureCycles uint16 + + // Sensitivity (in promille) for the dynamic threshold. + sensitivity uint16 + + // Capacitance trackers for dynamic capacitance measurement. + trackers []capacitanceTracker +} + +// Create a new array of pins to be used as touch sensors. +// The pins do not need to be initialized. The array is immediately ready to +// use. +// +// By default, NewArray configures a static threshold that is not very +// sensitive. If you want the touch inputs to be more sensitive, use +// SetDynamicThreshold. +func NewArray(pins []machine.Pin) *Array { + for _, pin := range pins { + pin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + pin.High() + } + array := &Array{ + pins: pins, + values: make([]uint16, len(pins)), + measureCycles: uint16(machine.CPUFrequency() / 125000), // 1000 on the RP2040 (which is 125MHz) + lastUpdate: time.Now(), + } + + // A threshold of 500 works well on the RP2040. Scale this number to + // something similar on other chips. + array.SetStaticThreshold(int(machine.CPUFrequency() / 250000)) + + return array +} + +// Use a static threshold. This works well on simple touch surfaces where you'll +// directly touch the metal. +func (a *Array) SetStaticThreshold(threshold int) { + if threshold > 0xffff { + threshold = 0xffff + } + a.staticThreshold = uint16(threshold) + a.trackers = nil +} + +// Use a dynamic threshold (as promille), that will calibrate automatically. +// This is needed when you want to be able to detect touches through a +// non-conducting surface for example. Something like 100‰ (10%) will probably +// work in many cases, though you may need to try different value to reliably +// detect touches. +func (a *Array) SetDynamicThreshold(sensitivity int) { + a.sensitivity = uint16(sensitivity) + a.staticThreshold = 0 + a.trackers = make([]capacitanceTracker, len(a.pins)) +} + +// Measure all GPIO pins. This function must be called very often, ideally about +// 100-200 times per second (it will delay a bit when called more than 200 times +// per second). +func (a *Array) Update() { + // Wait until enough time has passed to charge all pins. + now := time.Now() + timeSinceLastUpdate := now.Sub(a.lastUpdate) + sleepTime := minTimeBetweenMeasurements - timeSinceLastUpdate + time.Sleep(sleepTime) + a.lastUpdate = now.Add(sleepTime) // should be ~equivalent to time.Now() + + // Measure each pin in turn. + for i, pin := range a.pins { + // Interrupts must be disabled during measuring for accurate results. + mask := interrupt.Disable() + + // Switch to input. This will stop the charging, and let it discharge + // through the resistor. + pin.Configure(machine.PinConfig{Mode: machine.PinInput}) + + // Wait for the pin to go low again. + // A longer duration means more capacitance, which means something is + // touching it (finger, banana, etc). + count := uint32(i) + for i := 0; i < int(a.measureCycles); i++ { + if !pin.Get() { + break + } + count++ + } + + interrupt.Restore(mask) + + a.values[i] = uint16(count) + + // Set the pin to high, to charge it for the next measurement. + pin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + pin.High() + } + + // The first measurement tends to be slightly off (too low value) so ignore + // that one. + if !a.hasFirstMeasurement { + a.hasFirstMeasurement = true + return + } + + for i := 0; i < len(a.trackers); i++ { + a.trackers[i].addValue(int(a.values[i]), int(a.sensitivity)) + } +} + +// Return the raw value of the given pin index of the most recent call to +// Update. This value is not smoothed in any way. +func (a *Array) RawValue(index int) int { + return int(a.values[index]) +} + +// Return the value from the moving average. This value is only available when a +// dynamic threshold has been set, it will panic otherwise. +func (a *Array) SmoothedValue(index int) int { + return int(a.trackers[index].avg) / oversampling +} + +// Return whether the given pin index is currently being touched. +func (a *Array) Touching(index int) bool { + if a.staticThreshold != 0 { + // Using a static threshold. + return a.values[index] > a.staticThreshold + } + + return a.trackers[index].touching +} + +// Separate object to store calibration data and track capacitance over time. +type capacitanceTracker struct { + recentValues [movingAverageWindow]uint16 + sum uint32 + avg uint16 + + baseline uint16 + noise uint16 + valueCount uint8 + touching bool + + recalibrationCount uint8 + recalibrationPrevAvg uint16 + recalibrationNoiseSum int32 + recalibrationSum uint32 +} + +func (ct *capacitanceTracker) addValue(value int, sensitivity int) { + // Maybe increase the resolution slightly by oversampling. This should + // increase the resolution a little bit after averaging and should reduce + // rounding errors. + // Typical input values on the RP2040 are 100-200 (or up to 1000 or so when + // touching the metal) so multiplying by 4-8 should be fine. Other chips + // generally have much lower values. + value *= oversampling + if value > 0xffff { + value = 0xffff // unlikely, but make sure we don't overflow + } + + // This does a number of things at the same time: + // * Add the new value to the recentValues array. + // * Calculate the moving sum (and average) of recentValues using a + // recursive moving average algorithm: + // https://www.dspguide.com/ch15/5.htm + ptr := &ct.recentValues[ct.valueCount%movingAverageWindow] + ct.sum -= uint32(*ptr) + ct.sum += uint32(value) + ct.avg = uint16(ct.sum / movingAverageWindow) + *ptr = uint16(value) + ct.valueCount++ + + // Do an initial calibration once the first values have been read. + if ct.baseline == 0 && ct.valueCount == movingAverageWindow { + ct.baseline = ct.avg + + // Calculate initial noise as an average absolute deviation: + // https://en.wikipedia.org/wiki/Average_absolute_deviation + // This is a quick and imprecise way to find the noise, better noise + // detection happens during recalibration. + var diffSum uint32 + for _, sample := range ct.recentValues { + diff := int(ct.avg) - int(sample) + if diff < 0 { + diff = -diff + } + diffSum += uint32(diff) + } + ct.noise = uint16(diffSum / (movingAverageWindow / 2)) + } + + // Now determine whether the touch pad is being touched. + + if ct.baseline == 0 { + // Not yet calibrated. + ct.touching = false + return + } + + // Calculate the threshold. + // Divide by 65536 (instead of 65500) to avoid a potentially expensive + // division while still being close enough. + threshold := (uint32(ct.baseline) * uint32(sensitivity+1000) * 65) / 65536 + + // Add noise to the threshold, to avoid toggling quickly. This mainly + // filters out mains noise. + threshold += uint32(ct.noise) + + // Implement some hysteresis: if the touch pad was previously touched, lower + // the threshold a little to avoid bouncing effects. + // TODO: let this hysteresis depend on the amount of noise. + if ct.touching { + threshold = (threshold*3 + uint32(ct.baseline)) / 4 // lower the threshold by 25% + } + + // Is the pad being touched? + ct.touching = uint32(ct.avg) > threshold + + // Do a recalibration after the sensor hasn't been touched for ~5s, to + // account for drift over time (humidity etc). + if ct.touching { + // Reset calibration (start from zero). + ct.recalibrationCount = 0 + ct.recalibrationSum = 0 + ct.recalibrationNoiseSum = 0 + } else { + // Add the last batch of samples to the sum. + if ct.valueCount%movingAverageWindow == 0 { + ct.recalibrationCount++ + + // Wait a few cycles before starting data collection for + // calibration. + cycle := int(ct.recalibrationCount) - 3 + + if cycle < 0 { + // Store the previous average, to calculate the noise value. + ct.recalibrationPrevAvg = ct.avg + + } else if cycle >= 0 { + // Collect data for recalibration. + ct.recalibrationSum += ct.sum + + // Add difference between two (averaged) samples as a measure of + // the noise. + diff := int32(ct.recalibrationPrevAvg) - int32(ct.avg) + if diff < 0 { + diff = -diff + } + ct.recalibrationNoiseSum += diff + ct.recalibrationPrevAvg = ct.avg + + } + + // Do the recalibration after enough samples have been collected. + // Note: the noise is basically the average of absolute differences + // between two averaging windows. I don't know whether this + // algorithm has a name, but it seems to work here to detect the + // amount of noise. + const totalRecalibrationCount = recalibrationSamples / movingAverageWindow + if cycle == totalRecalibrationCount { + ct.baseline = uint16(ct.recalibrationSum / recalibrationSamples) + ct.noise = uint16(ct.recalibrationNoiseSum / (totalRecalibrationCount / 2)) + ct.recalibrationCount = 0 + ct.recalibrationSum = 0 + ct.recalibrationNoiseSum = 0 + } + } + } +} From 829ae09651f6fb813ca403bc1b61364eec22773a Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sat, 26 Oct 2024 09:57:45 +0200 Subject: [PATCH 039/137] ssd1306: add FillRectangle() and SetScroll() functions to satisfy tinyterm.Displayer interface Signed-off-by: deadprogram --- ssd1306/ssd1306.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ssd1306/ssd1306.go b/ssd1306/ssd1306.go index 2c5d9c3..e012918 100644 --- a/ssd1306/ssd1306.go +++ b/ssd1306/ssd1306.go @@ -396,3 +396,31 @@ func (d *Device) Sleep(sleepEnabled bool) error { } return nil } + +// FillRectangle fills a rectangle at a given coordinates with a color +func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error { + dw, dh := d.Size() + + if x < 0 || y < 0 || width <= 0 || height <= 0 || + x >= d.width || (x+width) > dw || y >= dh || (y+height) > dh { + return errOutOfRange + } + + if x+width == dw && y+height == dh && c.R == 0 && c.G == 0 && c.B == 0 { + d.ClearDisplay() + return nil + } + + for i := x; i < x+width; i++ { + for j := y; j < y+height; j++ { + d.SetPixel(i, j, c) + } + } + + return nil +} + +// SetScroll sets the vertical scrolling for the display, which is a NOP for this display. +func (d *Device) SetScroll(line int16) { + return +} From f12454d4f7bedf8060c1cee00be5f8a0ba1c55c8 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Fri, 25 Oct 2024 20:11:27 +0200 Subject: [PATCH 040/137] uc8151: add FillRectangle() and SetScroll() functions to satisfy tinyterm.Displayer interface Signed-off-by: deadprogram --- uc8151/uc8151.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/uc8151/uc8151.go b/uc8151/uc8151.go index 0c658d1..38c48f9 100644 --- a/uc8151/uc8151.go +++ b/uc8151/uc8151.go @@ -478,3 +478,31 @@ func (d *Device) SetLUT(speed Speed, flickerFree bool) error { return nil } + +// FillRectangle fills a rectangle at a given coordinates with a color +func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error { + dw, dh := d.Size() + + if x < 0 || y < 0 || width <= 0 || height <= 0 || + x >= d.width || (x+width) > dw || y >= dh || (y+height) > dh { + return errOutOfRange + } + + if x+width == dw && y+height == dh && c.R == 0 && c.G == 0 && c.B == 0 { + d.ClearDisplay() + return nil + } + + for i := x; i < x+width; i++ { + for j := y; j < y+height; j++ { + d.SetPixel(i, j, c) + } + } + + return nil +} + +// SetScroll sets the vertical scrolling for the display, which is a NOP for this display. +func (d *Device) SetScroll(line int16) { + return +} From 4fb5d7a0e57d6f94070984c78cd6a2f8c45d713b Mon Sep 17 00:00:00 2001 From: Ron Evans Date: Sun, 27 Oct 2024 23:59:39 +0000 Subject: [PATCH 041/137] pixel: add NewImageFromBytes function (#713) * pixel: add NewImageFromBytes() function to allow creating image from existing slice Signed-off-by: deadprogram * pixel: correct and clarify code for monochrome get/set pixels Signed-off-by: deadprogram --------- Signed-off-by: deadprogram --- pixel/image.go | 48 ++++++++++++++++++--- pixel/image_test.go | 102 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 134 insertions(+), 16 deletions(-) diff --git a/pixel/image.go b/pixel/image.go index 1a1d26f..3275d63 100644 --- a/pixel/image.go +++ b/pixel/image.go @@ -43,6 +43,40 @@ func NewImage[T Color](width, height int) Image[T] { } } +// NewImageFromBytes creates a new image of the given size using an existing data slice of bytes. +func NewImageFromBytes[T Color](width, height int, buf []byte) Image[T] { + if width < 0 || height < 0 || int(int16(width)) != width || int(int16(height)) != height { + // The width/height are stored as 16-bit integers and should never be + // negative. + panic("NewImageFromBytes: width/height out of bounds") + } + var zeroColor T + var data unsafe.Pointer + switch { + case zeroColor.BitsPerPixel()%8 == 0: + // Typical formats like RGB888 and RGB565. + // Each color starts at a whole byte offset from the start. + if len(buf) != width*height*int(unsafe.Sizeof(zeroColor)) { + panic("NewImageFromBytes: data slice size mismatch") + } + data = unsafe.Pointer(&buf[0]) + default: + // Formats like RGB444 that have 12 bits per pixel. + // We access these as bytes, so allocate the buffer as a byte slice. + bufBits := width * height * zeroColor.BitsPerPixel() + bufBytes := (bufBits + 7) / 8 + if len(buf) != bufBytes { + panic("NewImageFromBytes: data slice size mismatch") + } + data = unsafe.Pointer(&buf[0]) + } + return Image[T]{ + width: int16(width), + height: int16(height), + data: data, + } +} + // Rescale returns a new Image buffer based on the img buffer. // The contents is undefined after the Rescale operation, and any modification // to the returned image will overwrite the underlying image buffer in undefined @@ -105,14 +139,15 @@ func (img Image[T]) setPixel(index int, c T) { case zeroColor.BitsPerPixel() == 1: // Monochrome. x := index % int(img.width) - y := index / int(img.width) - offset := x + (y/8)*int(img.width) + offset := index / 8 + ptr := (*byte)(unsafe.Add(img.data, offset)) if c != zeroColor { - *((*byte)(ptr)) |= 1 << uint8(y%8) + *((*byte)(ptr)) |= (1 << (7 - uint8(x%8))) } else { - *((*byte)(ptr)) &^= 1 << uint8(y%8) + *((*byte)(ptr)) &^= (1 << (7 - uint8(x%8))) } + return case zeroColor.BitsPerPixel()%8 == 0: // Each color starts at a whole byte offset. @@ -166,9 +201,10 @@ func (img Image[T]) Get(x, y int) T { case zeroColor.BitsPerPixel() == 1: // Monochrome. var c Monochrome - offset := x + (y/8)*int(img.width) + offset := index / 8 + bits := index - (offset * 8) ptr := (*byte)(unsafe.Add(img.data, offset)) - c = (*ptr >> uint8(y%8) & 0x1) == 1 + c = ((*ptr >> (7 - uint8(bits))) & 0x1) > 0 return any(c).(T) case zeroColor.BitsPerPixel()%8 == 0: // Colors like RGB565, RGB888, etc. diff --git a/pixel/image_test.go b/pixel/image_test.go index bb7afc8..5e4804c 100644 --- a/pixel/image_test.go +++ b/pixel/image_test.go @@ -66,9 +66,9 @@ func TestImageRGB444BE(t *testing.T) { } func TestImageMonochrome(t *testing.T) { - image := pixel.NewImage[pixel.Monochrome](5, 3) - if width, height := image.Size(); width != 5 && height != 3 { - t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height) + image := pixel.NewImage[pixel.Monochrome](128, 64) + if width, height := image.Size(); width != 128 && height != 64 { + t.Errorf("image.Size(): expected 128, 64 but got %d, %d", width, height) } for _, expected := range []color.RGBA{ {R: 0xff, G: 0xff, B: 0xff}, @@ -80,19 +80,101 @@ func TestImageMonochrome(t *testing.T) { {B: 0x00, A: 0xff}, } { encoded := pixel.NewColor[pixel.Monochrome](expected.R, expected.G, expected.B) - image.Set(4, 2, encoded) - actual := image.Get(4, 2).RGBA() + image.Set(5, 3, encoded) + actual := image.Get(5, 3).RGBA() switch { - case expected.R == 0 && expected.G == 0 && expected.B == 0: - // should be false eg black - if actual.R != 0 || actual.G != 0 || actual.B != 0 { - t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual) - } case int(expected.R)+int(expected.G)+int(expected.B) > 128*3: // should be true eg white if actual.R == 0 || actual.G == 0 || actual.B == 0 { t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual) } + default: + // should be false eg black + if actual.R != 0 || actual.G != 0 || actual.B != 0 { + t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual) + } + } + } +} + +// 128x128 +var rprofile = []byte{ + 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, + 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, + 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE8, 0x17, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, + 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xBF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x01, 0xF8, 0x00, 0x5F, 0xFF, 0xFF, 0xFC, 0x00, 0x02, 0x80, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x03, 0xFE, 0x03, 0xFF, 0xFD, 0xBF, 0xFF, 0x80, 0x1F, 0xE0, 0x3F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x03, 0xFE, 0x01, 0xFF, 0xF7, 0x6B, 0xFF, 0x80, 0x1F, 0xC0, 0x1F, 0xFF, 0xFE, 0x02, 0xFF, 0xFC, 0x03, 0xDF, 0x17, 0xFA, 0x00, 0x00, 0x37, 0xF0, 0x3F, 0xE0, 0x1F, 0xFF, 0xD0, + 0x00, 0x07, 0xFC, 0x07, 0x07, 0xBF, 0x00, 0x00, 0x00, 0x01, 0xFE, 0xF8, 0x78, 0x3F, 0xF8, 0x00, 0x00, 0x01, 0xFC, 0x06, 0x1B, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xEA, 0x78, 0x1F, 0x80, 0x00, + 0x00, 0x01, 0xFC, 0x03, 0x1B, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xE2, 0x68, 0x1F, 0xC0, 0x00, 0x00, 0x01, 0xFC, 0x07, 0x5B, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x07, 0xC4, 0x38, 0x1F, 0x80, 0x00, + 0x00, 0x01, 0xF8, 0x03, 0x3F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF7, 0x78, 0x3F, 0xC0, 0x00, 0x00, 0x01, 0xFC, 0x03, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x68, 0x1F, 0x80, 0x00, + 0x00, 0x01, 0xFC, 0x07, 0x9F, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x70, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x03, 0x9E, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x3E, 0xE8, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x01, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xE0, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x01, 0xFF, 0x55, 0xF8, 0x00, 0x00, 0x03, 0xEA, 0xFF, 0xC0, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x01, 0xFE, 0xAF, 0xF0, 0x00, 0x00, 0x03, 0xFD, 0xBF, 0xE0, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x01, 0xF8, 0x00, 0x7C, 0x00, 0x00, 0x07, 0x80, 0x03, 0xE0, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x03, 0xC1, 0xE0, 0x3E, 0x00, 0x00, 0x1F, 0x03, 0xC0, 0xF0, 0x1F, 0x80, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x82, 0xF8, 0x0F, 0x00, 0x00, 0x3E, 0x05, 0xE0, 0x7C, 0x1F, 0x80, 0x00, + 0x00, 0x01, 0xFC, 0x07, 0x01, 0xF8, 0x17, 0x00, 0x00, 0x3C, 0x09, 0xE0, 0x78, 0x1F, 0xC0, 0x00, 0x00, 0x03, 0xFC, 0x0F, 0x06, 0xFC, 0x07, 0x80, 0x00, 0x7C, 0x1D, 0xE0, 0x3C, 0x1F, 0xC0, 0x00, + 0x00, 0xFF, 0xFC, 0x1E, 0x03, 0xFC, 0x03, 0x80, 0x00, 0x78, 0x1F, 0xE0, 0x1E, 0x1F, 0xFE, 0x80, 0x2F, 0xFF, 0xFC, 0x1C, 0x07, 0xF8, 0x01, 0x80, 0x00, 0x60, 0x1F, 0xE0, 0x16, 0x1F, 0xFF, 0xF8, + 0x1F, 0xFF, 0xF8, 0x3E, 0x07, 0xFC, 0x01, 0xC0, 0x00, 0xE8, 0x1F, 0xE0, 0x1E, 0x1F, 0xFF, 0xEC, 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0xF8, 0x01, 0xC0, 0x00, 0xE0, 0x17, 0xE0, 0x07, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x38, 0x03, 0xE8, 0x00, 0xC0, 0x00, 0xC0, 0x07, 0xC0, 0x03, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0x38, 0x00, 0xC0, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x78, 0x01, 0xE0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x00, 0x03, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0x38, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xF8, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x68, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x01, 0x9F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x68, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, 0x3F, 0xFF, 0xF8, 0x70, 0x00, 0x00, 0x00, 0xC0, 0x01, 0xC0, 0x00, 0x00, 0x01, 0x9F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x68, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x38, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, + 0x07, 0xFF, 0xFC, 0x78, 0x00, 0x00, 0x00, 0xC0, 0x80, 0xC0, 0x00, 0x00, 0x03, 0x1F, 0xFF, 0xF8, 0x00, 0x37, 0xF8, 0x38, 0x00, 0x00, 0x01, 0xC7, 0xF0, 0xE0, 0x00, 0x00, 0x03, 0x9F, 0xFE, 0x00, + 0x00, 0x5F, 0xFC, 0x38, 0x00, 0x00, 0x01, 0xC3, 0xE8, 0xE0, 0x00, 0x00, 0x03, 0x1F, 0xFB, 0x00, 0x00, 0x01, 0xFC, 0x3C, 0x00, 0x00, 0x01, 0xC7, 0xF8, 0xE0, 0x00, 0x00, 0x07, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x3C, 0x00, 0x00, 0x03, 0x9F, 0xFC, 0x70, 0x00, 0x00, 0x07, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x1E, 0x00, 0x00, 0x03, 0xBF, 0xFE, 0x78, 0x00, 0x00, 0x1E, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x1E, 0x00, 0x00, 0x07, 0x9F, 0xFF, 0x78, 0x00, 0x00, 0x16, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x1E, 0x00, 0x00, 0x07, 0x73, 0xC3, 0x3C, 0x00, 0x00, 0x1E, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x60, 0x01, 0x9E, 0x00, 0x00, 0x3C, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x1F, 0x80, 0x00, 0x7E, 0x40, 0x01, 0x17, 0x80, 0x00, 0x7E, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x1F, 0x80, 0x00, 0x3C, 0x40, 0x01, 0x9F, 0x00, 0x00, 0x7C, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x1F, 0xC0, 0x00, 0xFC, 0x40, 0x01, 0x07, 0xC0, 0x00, 0xFC, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x1F, 0xF8, 0x0B, 0xE8, 0x7B, 0xFF, 0x81, 0xF8, 0x03, 0xFC, 0x1F, 0x80, 0x00, 0x00, 0x03, 0xF8, 0x1C, 0xFF, 0xFF, 0xC0, 0x3F, 0xFE, 0x00, 0xFF, 0xFF, 0xDE, 0x1F, 0xC0, 0x00, + 0x00, 0x05, 0xFC, 0x1C, 0xFF, 0xFF, 0x80, 0x7F, 0xDE, 0x00, 0xFF, 0x7F, 0xDC, 0x1F, 0x80, 0x00, 0x00, 0xFF, 0xFC, 0x1C, 0x3F, 0xFE, 0x00, 0x0E, 0xB8, 0x00, 0x3F, 0xFF, 0x1C, 0x1F, 0xFC, 0x00, + 0x2F, 0xFF, 0xF8, 0x1C, 0x00, 0x00, 0x00, 0x04, 0x98, 0x00, 0x01, 0x00, 0x1E, 0x1F, 0xFF, 0xF4, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x06, 0x98, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x1C, 0x00, 0x00, 0x00, 0x06, 0x98, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x02, 0xB8, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x1C, 0x00, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, + 0x7F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, + 0x7F, 0xFF, 0xFC, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x1F, 0xFF, 0xFE, 0x0B, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xF8, + 0x00, 0x7F, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFE, 0x00, 0x00, 0x01, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xC0, 0x00, + 0x00, 0x03, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x38, 0x15, 0xB7, 0x80, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x1C, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xF8, 0x3C, 0x16, 0xAB, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x16, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x3C, 0x3F, 0xFB, 0x80, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x1C, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x3C, 0x1F, 0xE9, 0x00, 0x00, 0x01, 0xF7, 0x80, 0x00, 0x1E, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x3C, 0x03, 0x82, 0x1D, 0xC6, 0x39, 0xC0, 0x07, 0x00, 0x14, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x3C, 0x03, 0x87, 0x14, 0x85, 0x15, 0xC1, 0x03, 0x80, 0x1E, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x3C, 0x03, 0x87, 0x9F, 0xE6, 0x3D, 0xC0, 0x1F, 0xC0, 0x1C, 0x1F, 0xC0, 0x00, + 0x00, 0xBF, 0xF8, 0x3C, 0x03, 0x83, 0x9F, 0xE7, 0x3F, 0x8D, 0x1E, 0xC0, 0x16, 0x1F, 0xD4, 0x00, 0x17, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0x9E, 0xE7, 0x79, 0xD7, 0xBC, 0xE0, 0x1C, 0x1F, 0xFF, 0xE8, + 0x1F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0x9C, 0xE3, 0x3F, 0x9F, 0xBC, 0xE0, 0x1E, 0x1F, 0xFF, 0xD0, 0x3F, 0xFF, 0xF8, 0x3C, 0x03, 0x83, 0x9E, 0xE7, 0x79, 0xC7, 0xBC, 0xE0, 0x16, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0xBC, 0xE3, 0xF9, 0xC3, 0xBC, 0xE0, 0x1C, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0x9E, 0xEB, 0xE9, 0xE3, 0xBD, 0xE0, 0x1E, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xF8, 0x3C, 0x03, 0x83, 0x9C, 0xE3, 0xF1, 0xC3, 0x9C, 0xC0, 0x1C, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0x9E, 0xE9, 0xE0, 0xFF, 0x9F, 0xC0, 0x16, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0xBC, 0x61, 0xE0, 0xFF, 0x07, 0xC0, 0x1E, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x1C, 0x3F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x17, 0xC0, 0x00, 0x00, 0x00, 0x16, 0x3F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x3F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x3F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFE, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x7F, 0xFF, 0xFE, + 0x2F, 0xFF, 0xFF, 0xBC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x80, 0x5F, 0xFF, 0xFF, 0xF8, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, + 0x01, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA0, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, + 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, + 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +} + +func TestImageFromBytesMonochrome(t *testing.T) { + image := pixel.NewImageFromBytes[pixel.Monochrome](128, 128, rprofile) + if width, height := image.Size(); width != 128 && height != 128 { + t.Errorf("image.Size(): expected 128, 128 but got %d, %d", width, height) + } + + raw := image.RawBuffer() + for i, b := range raw { + if b != rprofile[i] { + t.Fatalf("failed to roundtrip image. expected %v but got %v", rprofile[i], b) } } } From bcf3b84654977bccee6a6d7592d1c79a9acfe456 Mon Sep 17 00:00:00 2001 From: Daniel Esteban Date: Mon, 28 Oct 2024 01:11:52 +0100 Subject: [PATCH 042/137] Revert "pixel: add NewImageFromBytes function (#713)" (#714) This reverts commit 4fb5d7a0e57d6f94070984c78cd6a2f8c45d713b. --- pixel/image.go | 48 +++------------------ pixel/image_test.go | 102 +++++--------------------------------------- 2 files changed, 16 insertions(+), 134 deletions(-) diff --git a/pixel/image.go b/pixel/image.go index 3275d63..1a1d26f 100644 --- a/pixel/image.go +++ b/pixel/image.go @@ -43,40 +43,6 @@ func NewImage[T Color](width, height int) Image[T] { } } -// NewImageFromBytes creates a new image of the given size using an existing data slice of bytes. -func NewImageFromBytes[T Color](width, height int, buf []byte) Image[T] { - if width < 0 || height < 0 || int(int16(width)) != width || int(int16(height)) != height { - // The width/height are stored as 16-bit integers and should never be - // negative. - panic("NewImageFromBytes: width/height out of bounds") - } - var zeroColor T - var data unsafe.Pointer - switch { - case zeroColor.BitsPerPixel()%8 == 0: - // Typical formats like RGB888 and RGB565. - // Each color starts at a whole byte offset from the start. - if len(buf) != width*height*int(unsafe.Sizeof(zeroColor)) { - panic("NewImageFromBytes: data slice size mismatch") - } - data = unsafe.Pointer(&buf[0]) - default: - // Formats like RGB444 that have 12 bits per pixel. - // We access these as bytes, so allocate the buffer as a byte slice. - bufBits := width * height * zeroColor.BitsPerPixel() - bufBytes := (bufBits + 7) / 8 - if len(buf) != bufBytes { - panic("NewImageFromBytes: data slice size mismatch") - } - data = unsafe.Pointer(&buf[0]) - } - return Image[T]{ - width: int16(width), - height: int16(height), - data: data, - } -} - // Rescale returns a new Image buffer based on the img buffer. // The contents is undefined after the Rescale operation, and any modification // to the returned image will overwrite the underlying image buffer in undefined @@ -139,15 +105,14 @@ func (img Image[T]) setPixel(index int, c T) { case zeroColor.BitsPerPixel() == 1: // Monochrome. x := index % int(img.width) - offset := index / 8 - + y := index / int(img.width) + offset := x + (y/8)*int(img.width) ptr := (*byte)(unsafe.Add(img.data, offset)) if c != zeroColor { - *((*byte)(ptr)) |= (1 << (7 - uint8(x%8))) + *((*byte)(ptr)) |= 1 << uint8(y%8) } else { - *((*byte)(ptr)) &^= (1 << (7 - uint8(x%8))) + *((*byte)(ptr)) &^= 1 << uint8(y%8) } - return case zeroColor.BitsPerPixel()%8 == 0: // Each color starts at a whole byte offset. @@ -201,10 +166,9 @@ func (img Image[T]) Get(x, y int) T { case zeroColor.BitsPerPixel() == 1: // Monochrome. var c Monochrome - offset := index / 8 - bits := index - (offset * 8) + offset := x + (y/8)*int(img.width) ptr := (*byte)(unsafe.Add(img.data, offset)) - c = ((*ptr >> (7 - uint8(bits))) & 0x1) > 0 + c = (*ptr >> uint8(y%8) & 0x1) == 1 return any(c).(T) case zeroColor.BitsPerPixel()%8 == 0: // Colors like RGB565, RGB888, etc. diff --git a/pixel/image_test.go b/pixel/image_test.go index 5e4804c..bb7afc8 100644 --- a/pixel/image_test.go +++ b/pixel/image_test.go @@ -66,9 +66,9 @@ func TestImageRGB444BE(t *testing.T) { } func TestImageMonochrome(t *testing.T) { - image := pixel.NewImage[pixel.Monochrome](128, 64) - if width, height := image.Size(); width != 128 && height != 64 { - t.Errorf("image.Size(): expected 128, 64 but got %d, %d", width, height) + image := pixel.NewImage[pixel.Monochrome](5, 3) + if width, height := image.Size(); width != 5 && height != 3 { + t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height) } for _, expected := range []color.RGBA{ {R: 0xff, G: 0xff, B: 0xff}, @@ -80,101 +80,19 @@ func TestImageMonochrome(t *testing.T) { {B: 0x00, A: 0xff}, } { encoded := pixel.NewColor[pixel.Monochrome](expected.R, expected.G, expected.B) - image.Set(5, 3, encoded) - actual := image.Get(5, 3).RGBA() + image.Set(4, 2, encoded) + actual := image.Get(4, 2).RGBA() switch { + case expected.R == 0 && expected.G == 0 && expected.B == 0: + // should be false eg black + if actual.R != 0 || actual.G != 0 || actual.B != 0 { + t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual) + } case int(expected.R)+int(expected.G)+int(expected.B) > 128*3: // should be true eg white if actual.R == 0 || actual.G == 0 || actual.B == 0 { t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual) } - default: - // should be false eg black - if actual.R != 0 || actual.G != 0 || actual.B != 0 { - t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual) - } - } - } -} - -// 128x128 -var rprofile = []byte{ - 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, - 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, - 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, - 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE8, 0x17, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, - 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, - 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFE, - 0x3F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFC, - 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, - 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFC, - 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xBF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, - 0x3F, 0xFF, 0xFC, 0x01, 0xF8, 0x00, 0x5F, 0xFF, 0xFF, 0xFC, 0x00, 0x02, 0x80, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x03, 0xFE, 0x03, 0xFF, 0xFD, 0xBF, 0xFF, 0x80, 0x1F, 0xE0, 0x3F, 0xFF, 0xFC, - 0x3F, 0xFF, 0xFC, 0x03, 0xFE, 0x01, 0xFF, 0xF7, 0x6B, 0xFF, 0x80, 0x1F, 0xC0, 0x1F, 0xFF, 0xFE, 0x02, 0xFF, 0xFC, 0x03, 0xDF, 0x17, 0xFA, 0x00, 0x00, 0x37, 0xF0, 0x3F, 0xE0, 0x1F, 0xFF, 0xD0, - 0x00, 0x07, 0xFC, 0x07, 0x07, 0xBF, 0x00, 0x00, 0x00, 0x01, 0xFE, 0xF8, 0x78, 0x3F, 0xF8, 0x00, 0x00, 0x01, 0xFC, 0x06, 0x1B, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xEA, 0x78, 0x1F, 0x80, 0x00, - 0x00, 0x01, 0xFC, 0x03, 0x1B, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xE2, 0x68, 0x1F, 0xC0, 0x00, 0x00, 0x01, 0xFC, 0x07, 0x5B, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x07, 0xC4, 0x38, 0x1F, 0x80, 0x00, - 0x00, 0x01, 0xF8, 0x03, 0x3F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF7, 0x78, 0x3F, 0xC0, 0x00, 0x00, 0x01, 0xFC, 0x03, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x68, 0x1F, 0x80, 0x00, - 0x00, 0x01, 0xFC, 0x07, 0x9F, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x70, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x03, 0x9E, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x3E, 0xE8, 0x1F, 0xC0, 0x00, - 0x00, 0x01, 0xFC, 0x01, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xE0, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x01, 0xFF, 0x55, 0xF8, 0x00, 0x00, 0x03, 0xEA, 0xFF, 0xC0, 0x1F, 0xC0, 0x00, - 0x00, 0x01, 0xFC, 0x01, 0xFE, 0xAF, 0xF0, 0x00, 0x00, 0x03, 0xFD, 0xBF, 0xE0, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x01, 0xF8, 0x00, 0x7C, 0x00, 0x00, 0x07, 0x80, 0x03, 0xE0, 0x1F, 0xC0, 0x00, - 0x00, 0x01, 0xFC, 0x03, 0xC1, 0xE0, 0x3E, 0x00, 0x00, 0x1F, 0x03, 0xC0, 0xF0, 0x1F, 0x80, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x82, 0xF8, 0x0F, 0x00, 0x00, 0x3E, 0x05, 0xE0, 0x7C, 0x1F, 0x80, 0x00, - 0x00, 0x01, 0xFC, 0x07, 0x01, 0xF8, 0x17, 0x00, 0x00, 0x3C, 0x09, 0xE0, 0x78, 0x1F, 0xC0, 0x00, 0x00, 0x03, 0xFC, 0x0F, 0x06, 0xFC, 0x07, 0x80, 0x00, 0x7C, 0x1D, 0xE0, 0x3C, 0x1F, 0xC0, 0x00, - 0x00, 0xFF, 0xFC, 0x1E, 0x03, 0xFC, 0x03, 0x80, 0x00, 0x78, 0x1F, 0xE0, 0x1E, 0x1F, 0xFE, 0x80, 0x2F, 0xFF, 0xFC, 0x1C, 0x07, 0xF8, 0x01, 0x80, 0x00, 0x60, 0x1F, 0xE0, 0x16, 0x1F, 0xFF, 0xF8, - 0x1F, 0xFF, 0xF8, 0x3E, 0x07, 0xFC, 0x01, 0xC0, 0x00, 0xE8, 0x1F, 0xE0, 0x1E, 0x1F, 0xFF, 0xEC, 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0xF8, 0x01, 0xC0, 0x00, 0xE0, 0x17, 0xE0, 0x07, 0x1F, 0xFF, 0xFE, - 0x3F, 0xFF, 0xFC, 0x38, 0x03, 0xE8, 0x00, 0xC0, 0x00, 0xC0, 0x07, 0xC0, 0x03, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0x38, 0x00, 0xC0, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, - 0x3F, 0xFF, 0xFC, 0x78, 0x01, 0xE0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x00, 0x03, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0x38, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, - 0x3F, 0xFF, 0xF8, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x68, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFC, - 0x3F, 0xFF, 0xFC, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x01, 0x9F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, - 0x3F, 0xFF, 0xFC, 0x68, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, 0x3F, 0xFF, 0xF8, 0x70, 0x00, 0x00, 0x00, 0xC0, 0x01, 0xC0, 0x00, 0x00, 0x01, 0x9F, 0xFF, 0xFC, - 0x3F, 0xFF, 0xFC, 0x68, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x38, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, - 0x07, 0xFF, 0xFC, 0x78, 0x00, 0x00, 0x00, 0xC0, 0x80, 0xC0, 0x00, 0x00, 0x03, 0x1F, 0xFF, 0xF8, 0x00, 0x37, 0xF8, 0x38, 0x00, 0x00, 0x01, 0xC7, 0xF0, 0xE0, 0x00, 0x00, 0x03, 0x9F, 0xFE, 0x00, - 0x00, 0x5F, 0xFC, 0x38, 0x00, 0x00, 0x01, 0xC3, 0xE8, 0xE0, 0x00, 0x00, 0x03, 0x1F, 0xFB, 0x00, 0x00, 0x01, 0xFC, 0x3C, 0x00, 0x00, 0x01, 0xC7, 0xF8, 0xE0, 0x00, 0x00, 0x07, 0x1F, 0xC0, 0x00, - 0x00, 0x01, 0xFC, 0x3C, 0x00, 0x00, 0x03, 0x9F, 0xFC, 0x70, 0x00, 0x00, 0x07, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x1E, 0x00, 0x00, 0x03, 0xBF, 0xFE, 0x78, 0x00, 0x00, 0x1E, 0x1F, 0xC0, 0x00, - 0x00, 0x01, 0xFC, 0x1E, 0x00, 0x00, 0x07, 0x9F, 0xFF, 0x78, 0x00, 0x00, 0x16, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x1E, 0x00, 0x00, 0x07, 0x73, 0xC3, 0x3C, 0x00, 0x00, 0x1E, 0x1F, 0xC0, 0x00, - 0x00, 0x01, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x60, 0x01, 0x9E, 0x00, 0x00, 0x3C, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x1F, 0x80, 0x00, 0x7E, 0x40, 0x01, 0x17, 0x80, 0x00, 0x7E, 0x1F, 0xC0, 0x00, - 0x00, 0x01, 0xFC, 0x1F, 0x80, 0x00, 0x3C, 0x40, 0x01, 0x9F, 0x00, 0x00, 0x7C, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x1F, 0xC0, 0x00, 0xFC, 0x40, 0x01, 0x07, 0xC0, 0x00, 0xFC, 0x1F, 0xC0, 0x00, - 0x00, 0x01, 0xFC, 0x1F, 0xF8, 0x0B, 0xE8, 0x7B, 0xFF, 0x81, 0xF8, 0x03, 0xFC, 0x1F, 0x80, 0x00, 0x00, 0x03, 0xF8, 0x1C, 0xFF, 0xFF, 0xC0, 0x3F, 0xFE, 0x00, 0xFF, 0xFF, 0xDE, 0x1F, 0xC0, 0x00, - 0x00, 0x05, 0xFC, 0x1C, 0xFF, 0xFF, 0x80, 0x7F, 0xDE, 0x00, 0xFF, 0x7F, 0xDC, 0x1F, 0x80, 0x00, 0x00, 0xFF, 0xFC, 0x1C, 0x3F, 0xFE, 0x00, 0x0E, 0xB8, 0x00, 0x3F, 0xFF, 0x1C, 0x1F, 0xFC, 0x00, - 0x2F, 0xFF, 0xF8, 0x1C, 0x00, 0x00, 0x00, 0x04, 0x98, 0x00, 0x01, 0x00, 0x1E, 0x1F, 0xFF, 0xF4, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x06, 0x98, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, - 0x3F, 0xFF, 0xFC, 0x1C, 0x00, 0x00, 0x00, 0x06, 0x98, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x02, 0xB8, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, - 0x3F, 0xFF, 0xFC, 0x1C, 0x00, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, - 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, - 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, - 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, - 0x7F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, - 0x7F, 0xFF, 0xFC, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x1F, 0xFF, 0xFE, 0x0B, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xF8, - 0x00, 0x7F, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFE, 0x00, 0x00, 0x01, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xC0, 0x00, - 0x00, 0x03, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xC0, 0x00, - 0x00, 0x01, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x38, 0x15, 0xB7, 0x80, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x1C, 0x1F, 0xC0, 0x00, - 0x00, 0x01, 0xF8, 0x3C, 0x16, 0xAB, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x16, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x3C, 0x3F, 0xFB, 0x80, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x1C, 0x1F, 0xC0, 0x00, - 0x00, 0x01, 0xFC, 0x3C, 0x1F, 0xE9, 0x00, 0x00, 0x01, 0xF7, 0x80, 0x00, 0x1E, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x3C, 0x03, 0x82, 0x1D, 0xC6, 0x39, 0xC0, 0x07, 0x00, 0x14, 0x1F, 0xC0, 0x00, - 0x00, 0x01, 0xFC, 0x3C, 0x03, 0x87, 0x14, 0x85, 0x15, 0xC1, 0x03, 0x80, 0x1E, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x3C, 0x03, 0x87, 0x9F, 0xE6, 0x3D, 0xC0, 0x1F, 0xC0, 0x1C, 0x1F, 0xC0, 0x00, - 0x00, 0xBF, 0xF8, 0x3C, 0x03, 0x83, 0x9F, 0xE7, 0x3F, 0x8D, 0x1E, 0xC0, 0x16, 0x1F, 0xD4, 0x00, 0x17, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0x9E, 0xE7, 0x79, 0xD7, 0xBC, 0xE0, 0x1C, 0x1F, 0xFF, 0xE8, - 0x1F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0x9C, 0xE3, 0x3F, 0x9F, 0xBC, 0xE0, 0x1E, 0x1F, 0xFF, 0xD0, 0x3F, 0xFF, 0xF8, 0x3C, 0x03, 0x83, 0x9E, 0xE7, 0x79, 0xC7, 0xBC, 0xE0, 0x16, 0x1F, 0xFF, 0xFE, - 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0xBC, 0xE3, 0xF9, 0xC3, 0xBC, 0xE0, 0x1C, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0x9E, 0xEB, 0xE9, 0xE3, 0xBD, 0xE0, 0x1E, 0x1F, 0xFF, 0xFE, - 0x3F, 0xFF, 0xF8, 0x3C, 0x03, 0x83, 0x9C, 0xE3, 0xF1, 0xC3, 0x9C, 0xC0, 0x1C, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0x9E, 0xE9, 0xE0, 0xFF, 0x9F, 0xC0, 0x16, 0x1F, 0xFF, 0xFE, - 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0xBC, 0x61, 0xE0, 0xFF, 0x07, 0xC0, 0x1E, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x1C, 0x3F, 0xFF, 0xFC, - 0x3F, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFC, - 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x17, 0xC0, 0x00, 0x00, 0x00, 0x16, 0x3F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x3F, 0xFF, 0xFE, - 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x3F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFE, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x7F, 0xFF, 0xFE, - 0x2F, 0xFF, 0xFF, 0xBC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x80, 0x5F, 0xFF, 0xFF, 0xF8, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, - 0x01, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA0, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, - 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, - 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -} - -func TestImageFromBytesMonochrome(t *testing.T) { - image := pixel.NewImageFromBytes[pixel.Monochrome](128, 128, rprofile) - if width, height := image.Size(); width != 128 && height != 128 { - t.Errorf("image.Size(): expected 128, 128 but got %d, %d", width, height) - } - - raw := image.RawBuffer() - for i, b := range raw { - if b != rprofile[i] { - t.Fatalf("failed to roundtrip image. expected %v but got %v", rprofile[i], b) } } } From ecae5e28ad26f899286024784578d93cd486d6ed Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sat, 26 Oct 2024 18:00:33 +0200 Subject: [PATCH 043/137] pixel: add NewImageFromBytes() function to allow creating image from existing slice Signed-off-by: deadprogram --- pixel/image.go | 34 +++++++++++++++ pixel/image_test.go | 102 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 126 insertions(+), 10 deletions(-) diff --git a/pixel/image.go b/pixel/image.go index 1a1d26f..58651f4 100644 --- a/pixel/image.go +++ b/pixel/image.go @@ -43,6 +43,40 @@ func NewImage[T Color](width, height int) Image[T] { } } +// NewImageFromBytes creates a new image of the given size using an existing data slice of bytes. +func NewImageFromBytes[T Color](width, height int, buf []byte) Image[T] { + if width < 0 || height < 0 || int(int16(width)) != width || int(int16(height)) != height { + // The width/height are stored as 16-bit integers and should never be + // negative. + panic("NewImageFromBytes: width/height out of bounds") + } + var zeroColor T + var data unsafe.Pointer + switch { + case zeroColor.BitsPerPixel()%8 == 0: + // Typical formats like RGB888 and RGB565. + // Each color starts at a whole byte offset from the start. + if len(buf) != width*height*int(unsafe.Sizeof(zeroColor)) { + panic("NewImageFromBytes: data slice size mismatch") + } + data = unsafe.Pointer(&buf[0]) + default: + // Formats like RGB444 that have 12 bits per pixel. + // We access these as bytes, so allocate the buffer as a byte slice. + bufBits := width * height * zeroColor.BitsPerPixel() + bufBytes := (bufBits + 7) / 8 + if len(buf) != bufBytes { + panic("NewImageFromBytes: data slice size mismatch") + } + data = unsafe.Pointer(&buf[0]) + } + return Image[T]{ + width: int16(width), + height: int16(height), + data: data, + } +} + // Rescale returns a new Image buffer based on the img buffer. // The contents is undefined after the Rescale operation, and any modification // to the returned image will overwrite the underlying image buffer in undefined diff --git a/pixel/image_test.go b/pixel/image_test.go index bb7afc8..5e4804c 100644 --- a/pixel/image_test.go +++ b/pixel/image_test.go @@ -66,9 +66,9 @@ func TestImageRGB444BE(t *testing.T) { } func TestImageMonochrome(t *testing.T) { - image := pixel.NewImage[pixel.Monochrome](5, 3) - if width, height := image.Size(); width != 5 && height != 3 { - t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height) + image := pixel.NewImage[pixel.Monochrome](128, 64) + if width, height := image.Size(); width != 128 && height != 64 { + t.Errorf("image.Size(): expected 128, 64 but got %d, %d", width, height) } for _, expected := range []color.RGBA{ {R: 0xff, G: 0xff, B: 0xff}, @@ -80,19 +80,101 @@ func TestImageMonochrome(t *testing.T) { {B: 0x00, A: 0xff}, } { encoded := pixel.NewColor[pixel.Monochrome](expected.R, expected.G, expected.B) - image.Set(4, 2, encoded) - actual := image.Get(4, 2).RGBA() + image.Set(5, 3, encoded) + actual := image.Get(5, 3).RGBA() switch { - case expected.R == 0 && expected.G == 0 && expected.B == 0: - // should be false eg black - if actual.R != 0 || actual.G != 0 || actual.B != 0 { - t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual) - } case int(expected.R)+int(expected.G)+int(expected.B) > 128*3: // should be true eg white if actual.R == 0 || actual.G == 0 || actual.B == 0 { t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual) } + default: + // should be false eg black + if actual.R != 0 || actual.G != 0 || actual.B != 0 { + t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual) + } + } + } +} + +// 128x128 +var rprofile = []byte{ + 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, + 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, + 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE8, 0x17, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, + 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xBF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x01, 0xF8, 0x00, 0x5F, 0xFF, 0xFF, 0xFC, 0x00, 0x02, 0x80, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x03, 0xFE, 0x03, 0xFF, 0xFD, 0xBF, 0xFF, 0x80, 0x1F, 0xE0, 0x3F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x03, 0xFE, 0x01, 0xFF, 0xF7, 0x6B, 0xFF, 0x80, 0x1F, 0xC0, 0x1F, 0xFF, 0xFE, 0x02, 0xFF, 0xFC, 0x03, 0xDF, 0x17, 0xFA, 0x00, 0x00, 0x37, 0xF0, 0x3F, 0xE0, 0x1F, 0xFF, 0xD0, + 0x00, 0x07, 0xFC, 0x07, 0x07, 0xBF, 0x00, 0x00, 0x00, 0x01, 0xFE, 0xF8, 0x78, 0x3F, 0xF8, 0x00, 0x00, 0x01, 0xFC, 0x06, 0x1B, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xEA, 0x78, 0x1F, 0x80, 0x00, + 0x00, 0x01, 0xFC, 0x03, 0x1B, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xE2, 0x68, 0x1F, 0xC0, 0x00, 0x00, 0x01, 0xFC, 0x07, 0x5B, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x07, 0xC4, 0x38, 0x1F, 0x80, 0x00, + 0x00, 0x01, 0xF8, 0x03, 0x3F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF7, 0x78, 0x3F, 0xC0, 0x00, 0x00, 0x01, 0xFC, 0x03, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x68, 0x1F, 0x80, 0x00, + 0x00, 0x01, 0xFC, 0x07, 0x9F, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x70, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x03, 0x9E, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x3E, 0xE8, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x01, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xE0, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x01, 0xFF, 0x55, 0xF8, 0x00, 0x00, 0x03, 0xEA, 0xFF, 0xC0, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x01, 0xFE, 0xAF, 0xF0, 0x00, 0x00, 0x03, 0xFD, 0xBF, 0xE0, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x01, 0xF8, 0x00, 0x7C, 0x00, 0x00, 0x07, 0x80, 0x03, 0xE0, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x03, 0xC1, 0xE0, 0x3E, 0x00, 0x00, 0x1F, 0x03, 0xC0, 0xF0, 0x1F, 0x80, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x82, 0xF8, 0x0F, 0x00, 0x00, 0x3E, 0x05, 0xE0, 0x7C, 0x1F, 0x80, 0x00, + 0x00, 0x01, 0xFC, 0x07, 0x01, 0xF8, 0x17, 0x00, 0x00, 0x3C, 0x09, 0xE0, 0x78, 0x1F, 0xC0, 0x00, 0x00, 0x03, 0xFC, 0x0F, 0x06, 0xFC, 0x07, 0x80, 0x00, 0x7C, 0x1D, 0xE0, 0x3C, 0x1F, 0xC0, 0x00, + 0x00, 0xFF, 0xFC, 0x1E, 0x03, 0xFC, 0x03, 0x80, 0x00, 0x78, 0x1F, 0xE0, 0x1E, 0x1F, 0xFE, 0x80, 0x2F, 0xFF, 0xFC, 0x1C, 0x07, 0xF8, 0x01, 0x80, 0x00, 0x60, 0x1F, 0xE0, 0x16, 0x1F, 0xFF, 0xF8, + 0x1F, 0xFF, 0xF8, 0x3E, 0x07, 0xFC, 0x01, 0xC0, 0x00, 0xE8, 0x1F, 0xE0, 0x1E, 0x1F, 0xFF, 0xEC, 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0xF8, 0x01, 0xC0, 0x00, 0xE0, 0x17, 0xE0, 0x07, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x38, 0x03, 0xE8, 0x00, 0xC0, 0x00, 0xC0, 0x07, 0xC0, 0x03, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0x38, 0x00, 0xC0, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x78, 0x01, 0xE0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x00, 0x03, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0x38, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xF8, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x68, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x01, 0x9F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x68, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, 0x3F, 0xFF, 0xF8, 0x70, 0x00, 0x00, 0x00, 0xC0, 0x01, 0xC0, 0x00, 0x00, 0x01, 0x9F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x68, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x38, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, + 0x07, 0xFF, 0xFC, 0x78, 0x00, 0x00, 0x00, 0xC0, 0x80, 0xC0, 0x00, 0x00, 0x03, 0x1F, 0xFF, 0xF8, 0x00, 0x37, 0xF8, 0x38, 0x00, 0x00, 0x01, 0xC7, 0xF0, 0xE0, 0x00, 0x00, 0x03, 0x9F, 0xFE, 0x00, + 0x00, 0x5F, 0xFC, 0x38, 0x00, 0x00, 0x01, 0xC3, 0xE8, 0xE0, 0x00, 0x00, 0x03, 0x1F, 0xFB, 0x00, 0x00, 0x01, 0xFC, 0x3C, 0x00, 0x00, 0x01, 0xC7, 0xF8, 0xE0, 0x00, 0x00, 0x07, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x3C, 0x00, 0x00, 0x03, 0x9F, 0xFC, 0x70, 0x00, 0x00, 0x07, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x1E, 0x00, 0x00, 0x03, 0xBF, 0xFE, 0x78, 0x00, 0x00, 0x1E, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x1E, 0x00, 0x00, 0x07, 0x9F, 0xFF, 0x78, 0x00, 0x00, 0x16, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x1E, 0x00, 0x00, 0x07, 0x73, 0xC3, 0x3C, 0x00, 0x00, 0x1E, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x60, 0x01, 0x9E, 0x00, 0x00, 0x3C, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x1F, 0x80, 0x00, 0x7E, 0x40, 0x01, 0x17, 0x80, 0x00, 0x7E, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x1F, 0x80, 0x00, 0x3C, 0x40, 0x01, 0x9F, 0x00, 0x00, 0x7C, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x1F, 0xC0, 0x00, 0xFC, 0x40, 0x01, 0x07, 0xC0, 0x00, 0xFC, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x1F, 0xF8, 0x0B, 0xE8, 0x7B, 0xFF, 0x81, 0xF8, 0x03, 0xFC, 0x1F, 0x80, 0x00, 0x00, 0x03, 0xF8, 0x1C, 0xFF, 0xFF, 0xC0, 0x3F, 0xFE, 0x00, 0xFF, 0xFF, 0xDE, 0x1F, 0xC0, 0x00, + 0x00, 0x05, 0xFC, 0x1C, 0xFF, 0xFF, 0x80, 0x7F, 0xDE, 0x00, 0xFF, 0x7F, 0xDC, 0x1F, 0x80, 0x00, 0x00, 0xFF, 0xFC, 0x1C, 0x3F, 0xFE, 0x00, 0x0E, 0xB8, 0x00, 0x3F, 0xFF, 0x1C, 0x1F, 0xFC, 0x00, + 0x2F, 0xFF, 0xF8, 0x1C, 0x00, 0x00, 0x00, 0x04, 0x98, 0x00, 0x01, 0x00, 0x1E, 0x1F, 0xFF, 0xF4, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x06, 0x98, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x1C, 0x00, 0x00, 0x00, 0x06, 0x98, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x02, 0xB8, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x1C, 0x00, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, + 0x7F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, + 0x7F, 0xFF, 0xFC, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x1F, 0xFF, 0xFE, 0x0B, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xF8, + 0x00, 0x7F, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFE, 0x00, 0x00, 0x01, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xC0, 0x00, + 0x00, 0x03, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x38, 0x15, 0xB7, 0x80, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x1C, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xF8, 0x3C, 0x16, 0xAB, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x16, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x3C, 0x3F, 0xFB, 0x80, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x1C, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x3C, 0x1F, 0xE9, 0x00, 0x00, 0x01, 0xF7, 0x80, 0x00, 0x1E, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x3C, 0x03, 0x82, 0x1D, 0xC6, 0x39, 0xC0, 0x07, 0x00, 0x14, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x3C, 0x03, 0x87, 0x14, 0x85, 0x15, 0xC1, 0x03, 0x80, 0x1E, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x3C, 0x03, 0x87, 0x9F, 0xE6, 0x3D, 0xC0, 0x1F, 0xC0, 0x1C, 0x1F, 0xC0, 0x00, + 0x00, 0xBF, 0xF8, 0x3C, 0x03, 0x83, 0x9F, 0xE7, 0x3F, 0x8D, 0x1E, 0xC0, 0x16, 0x1F, 0xD4, 0x00, 0x17, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0x9E, 0xE7, 0x79, 0xD7, 0xBC, 0xE0, 0x1C, 0x1F, 0xFF, 0xE8, + 0x1F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0x9C, 0xE3, 0x3F, 0x9F, 0xBC, 0xE0, 0x1E, 0x1F, 0xFF, 0xD0, 0x3F, 0xFF, 0xF8, 0x3C, 0x03, 0x83, 0x9E, 0xE7, 0x79, 0xC7, 0xBC, 0xE0, 0x16, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0xBC, 0xE3, 0xF9, 0xC3, 0xBC, 0xE0, 0x1C, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0x9E, 0xEB, 0xE9, 0xE3, 0xBD, 0xE0, 0x1E, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xF8, 0x3C, 0x03, 0x83, 0x9C, 0xE3, 0xF1, 0xC3, 0x9C, 0xC0, 0x1C, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0x9E, 0xE9, 0xE0, 0xFF, 0x9F, 0xC0, 0x16, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0xBC, 0x61, 0xE0, 0xFF, 0x07, 0xC0, 0x1E, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x1C, 0x3F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x17, 0xC0, 0x00, 0x00, 0x00, 0x16, 0x3F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x3F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x3F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFE, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x7F, 0xFF, 0xFE, + 0x2F, 0xFF, 0xFF, 0xBC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x80, 0x5F, 0xFF, 0xFF, 0xF8, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, + 0x01, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA0, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, + 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, + 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +} + +func TestImageFromBytesMonochrome(t *testing.T) { + image := pixel.NewImageFromBytes[pixel.Monochrome](128, 128, rprofile) + if width, height := image.Size(); width != 128 && height != 128 { + t.Errorf("image.Size(): expected 128, 128 but got %d, %d", width, height) + } + + raw := image.RawBuffer() + for i, b := range raw { + if b != rprofile[i] { + t.Fatalf("failed to roundtrip image. expected %v but got %v", rprofile[i], b) } } } From 2f3b5ca59a06b91b09912c9c1fa76fd81d1f5106 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 27 Oct 2024 13:03:33 +0100 Subject: [PATCH 044/137] pixel: correct and clarify code for monochrome get/set pixels Signed-off-by: deadprogram --- pixel/image.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pixel/image.go b/pixel/image.go index 58651f4..3275d63 100644 --- a/pixel/image.go +++ b/pixel/image.go @@ -139,14 +139,15 @@ func (img Image[T]) setPixel(index int, c T) { case zeroColor.BitsPerPixel() == 1: // Monochrome. x := index % int(img.width) - y := index / int(img.width) - offset := x + (y/8)*int(img.width) + offset := index / 8 + ptr := (*byte)(unsafe.Add(img.data, offset)) if c != zeroColor { - *((*byte)(ptr)) |= 1 << uint8(y%8) + *((*byte)(ptr)) |= (1 << (7 - uint8(x%8))) } else { - *((*byte)(ptr)) &^= 1 << uint8(y%8) + *((*byte)(ptr)) &^= (1 << (7 - uint8(x%8))) } + return case zeroColor.BitsPerPixel()%8 == 0: // Each color starts at a whole byte offset. @@ -200,9 +201,10 @@ func (img Image[T]) Get(x, y int) T { case zeroColor.BitsPerPixel() == 1: // Monochrome. var c Monochrome - offset := x + (y/8)*int(img.width) + offset := index / 8 + bits := index - (offset * 8) ptr := (*byte)(unsafe.Add(img.data, offset)) - c = (*ptr >> uint8(y%8) & 0x1) == 1 + c = ((*ptr >> (7 - uint8(bits))) & 0x1) > 0 return any(c).(T) case zeroColor.BitsPerPixel()%8 == 0: // Colors like RGB565, RGB888, etc. From 0186d0905dcc2e15920e58383ec8df355bc356bd Mon Sep 17 00:00:00 2001 From: deadprogram Date: Mon, 28 Oct 2024 06:51:26 +0100 Subject: [PATCH 045/137] pixel: remove monochrome image from random noise test Signed-off-by: deadprogram --- pixel/image_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/pixel/image_test.go b/pixel/image_test.go index 5e4804c..f192427 100644 --- a/pixel/image_test.go +++ b/pixel/image_test.go @@ -194,9 +194,6 @@ func TestImageNoise(t *testing.T) { t.Run("RGB444BE", func(t *testing.T) { testImageNoise[pixel.RGB444BE](t) }) - t.Run("Monochrome", func(t *testing.T) { - testImageNoise[pixel.Monochrome](t) - }) } func testImageNoise[T pixel.Color](t *testing.T) { From 7d7efe25e71711297043572129ad0ae794c90432 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 28 Oct 2024 09:48:09 +0100 Subject: [PATCH 046/137] pixel: fix Monochrome setPixel Set/setPixel and Get weren't using the same indices. I've taken the ones used in Get and applied them to setPixel too. This fixes testImageNoise for the monochrome image. --- pixel/image.go | 8 ++++---- pixel/image_test.go | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pixel/image.go b/pixel/image.go index 3275d63..b3ef562 100644 --- a/pixel/image.go +++ b/pixel/image.go @@ -138,14 +138,14 @@ func (img Image[T]) setPixel(index int, c T) { switch { case zeroColor.BitsPerPixel() == 1: // Monochrome. - x := index % int(img.width) offset := index / 8 + bits := index % 8 ptr := (*byte)(unsafe.Add(img.data, offset)) if c != zeroColor { - *((*byte)(ptr)) |= (1 << (7 - uint8(x%8))) + *((*byte)(ptr)) |= (1 << (7 - uint8(bits))) } else { - *((*byte)(ptr)) &^= (1 << (7 - uint8(x%8))) + *((*byte)(ptr)) &^= (1 << (7 - uint8(bits))) } return @@ -202,7 +202,7 @@ func (img Image[T]) Get(x, y int) T { // Monochrome. var c Monochrome offset := index / 8 - bits := index - (offset * 8) + bits := index % 8 ptr := (*byte)(unsafe.Add(img.data, offset)) c = ((*ptr >> (7 - uint8(bits))) & 0x1) > 0 return any(c).(T) diff --git a/pixel/image_test.go b/pixel/image_test.go index f192427..5e4804c 100644 --- a/pixel/image_test.go +++ b/pixel/image_test.go @@ -194,6 +194,9 @@ func TestImageNoise(t *testing.T) { t.Run("RGB444BE", func(t *testing.T) { testImageNoise[pixel.RGB444BE](t) }) + t.Run("Monochrome", func(t *testing.T) { + testImageNoise[pixel.Monochrome](t) + }) } func testImageNoise[T pixel.Color](t *testing.T) { From 76a4276b5deaf6ef9639dc9b4dbc1ff01eca281c Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 28 Oct 2024 09:52:15 +0100 Subject: [PATCH 047/137] pixels: do more extensive testing for roundtrip bugs between Set and Get --- pixel/image_test.go | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/pixel/image_test.go b/pixel/image_test.go index 5e4804c..2b804b6 100644 --- a/pixel/image_test.go +++ b/pixel/image_test.go @@ -183,22 +183,30 @@ func TestImageFromBytesMonochrome(t *testing.T) { // contain the same data afterwards. func TestImageNoise(t *testing.T) { t.Run("RGB888", func(t *testing.T) { - testImageNoise[pixel.RGB888](t) + testImageNoiseN[pixel.RGB888](t) }) t.Run("RGB565BE", func(t *testing.T) { - testImageNoise[pixel.RGB565BE](t) + testImageNoiseN[pixel.RGB565BE](t) }) t.Run("RGB555", func(t *testing.T) { - testImageNoise[pixel.RGB555](t) + testImageNoiseN[pixel.RGB555](t) }) t.Run("RGB444BE", func(t *testing.T) { - testImageNoise[pixel.RGB444BE](t) + testImageNoiseN[pixel.RGB444BE](t) }) t.Run("Monochrome", func(t *testing.T) { - testImageNoise[pixel.Monochrome](t) + testImageNoiseN[pixel.Monochrome](t) }) } +// Run the testImageNoise multiple times, because a single test might not catch +// all bugs (since the test uses random data). +func testImageNoiseN[T pixel.Color](t *testing.T) { + for i := 0; i < 10; i++ { + testImageNoise[T](t) + } +} + func testImageNoise[T pixel.Color](t *testing.T) { // Create an image of a random width/height for extra testing. width := rand.Int()%500 + 10 From d04e68bef85839efbea55ed1a592d1dcd44f251e Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 29 Oct 2024 01:11:09 +0100 Subject: [PATCH 048/137] bugfix: correct casting of return value from adc as it was always overflowing the 8 bit result Signed-off-by: deadprogram --- pcf8591/pcf8591.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pcf8591/pcf8591.go b/pcf8591/pcf8591.go index cd57dee..3bb39ba 100644 --- a/pcf8591/pcf8591.go +++ b/pcf8591/pcf8591.go @@ -76,7 +76,7 @@ func (p ADCPin) Get() uint16 { p.d.bus.Tx(p.d.Address, tx, rx) // scale result to 16bit value like other ADCs - return uint16(rx[1] << 8) + return uint16(rx[1]) << 8 } // Configure here just for interface compatibility. From 019bbbe5fb66ff6ecd50949520253a50ecc72733 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 29 Oct 2024 20:25:00 +0100 Subject: [PATCH 049/137] Prepare for release 0.29 Signed-off-by: deadprogram --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ version.go | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4408f1b..fb065a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,40 @@ +0.29.0 +--- +- **new devices** + - **epd1in54** + - Waveshare 1.54inch B/W e-Paper display (#704) + - **touch** + - add capacitive touch sensing on normal GPIO pins + - **INA219** + - I2C INA219 driver (#705) + - **pcf8591** + - add ADC only implementation for I2C ADC/DAC (#690) + +- **enhancements** + - **pixel** + - add NewImageFromBytes() function to allow creating image from existing slice + - **servo** + - Add function `SetAngleWithMicroseconds` (#695) + - **onewire** + - onewire improvements + - **ssd1306** + - Add function `SetFlip` and `GetFlip` (#702) + - **uc8151** + - add FillRectangle() and SetScroll() functions to satisfy tinyterm.Displayer interface + - **ssd1306** + - add FillRectangle() and SetScroll() functions to satisfy tinyterm.Displayer interface + +- **bugfixes** + - **pixel** + - fix Monochrome setPixel + +- **docs** + - **readme** + - discuss need to change variables in examples + - **sponsor** + - Add sponsor button to key repositories + + 0.28.0 --- - **new devices** diff --git a/version.go b/version.go index 2019a9d..da56caa 100644 --- a/version.go +++ b/version.go @@ -2,4 +2,4 @@ package drivers // Version returns a user-readable string showing the version of the drivers package for support purposes. // Update this value before release of new version of software. -const Version = "0.28.0" +const Version = "0.29.0" From 0623bb425c6825bbe07a277cc69f4c447a60f6b6 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 18 Dec 2024 16:18:54 +0100 Subject: [PATCH 050/137] chore: update microphone driver to use latest i2s interface Signed-off-by: deadprogram --- microphone/microphone.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/microphone/microphone.go b/microphone/microphone.go index 03d82b3..60a0e3f 100644 --- a/microphone/microphone.go +++ b/microphone/microphone.go @@ -64,7 +64,7 @@ func (d *Device) Read(r []int32) (int, error) { count := len(r) // get the next group of samples - machine.I2S0.Read(d.buf) + machine.I2S0.ReadStereo(d.buf) if len(r) > len(d.buf) { count = len(d.buf) @@ -83,7 +83,7 @@ func (d *Device) ReadWithFilter(r []int32) (int, error) { for i := 0; i < len(r); i++ { // get the next group of samples - machine.I2S0.Read(d.buf) + machine.I2S0.ReadStereo(d.buf) // filter sum = applySincFilter(d.buf) From 17f273243d673c7d113c0bb1a3fb4d70f851274c Mon Sep 17 00:00:00 2001 From: ehime-iyokan Date: Mon, 4 Nov 2024 12:22:58 +0900 Subject: [PATCH 051/137] hub75: fix data buffering --- hub75/hub75.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hub75/hub75.go b/hub75/hub75.go index 8205a53..427d849 100644 --- a/hub75/hub75.go +++ b/hub75/hub75.go @@ -167,7 +167,7 @@ func (d *Device) fillMatrixBuffer(x int16, y int16, r uint8, g uint8, b uint8) { if r > colorTresh { d.buffer[c][offsetR] |= 1 << bitSelect } else { - d.buffer[c][offsetR] = d.buffer[c][offsetR] &^ 1 << bitSelect + d.buffer[c][offsetR] &^= 1 << bitSelect } if g > colorTresh { d.buffer[(c+d.colorThirdStep)%d.colorDepth][offsetG] |= 1 << bitSelect From 00578a3a81f39f644d2828e921e15548202c19df Mon Sep 17 00:00:00 2001 From: Triston Whetten <@users.noreply.github.com> Date: Sun, 12 Jan 2025 18:53:59 -0700 Subject: [PATCH 052/137] Fixed bug in aht20 driver --- aht20/aht20.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aht20/aht20.go b/aht20/aht20.go index 608f5ef..7713d27 100644 --- a/aht20/aht20.go +++ b/aht20/aht20.go @@ -29,7 +29,7 @@ func New(bus drivers.I2C) Device { func (d *Device) Configure() { // Check initialization state status := d.Status() - if status&0x08 == 1 { + if status&STATUS_CALIBRATED == 1 { // Device is initialized return } @@ -69,7 +69,7 @@ func (d *Device) Read() error { } // If measurement complete, store values - if data[0]&0x04 != 0 && data[0]&0x80 == 0 { + if data[0]&STATUS_CALIBRATED != 0 && data[0]&STATUS_BUSY == 0 { d.humidity = uint32(data[1])<<12 | uint32(data[2])<<4 | uint32(data[3])>>4 d.temp = (uint32(data[3])&0xF)<<16 | uint32(data[4])<<8 | uint32(data[5]) return nil From 486949686cf268305c46422328727cd151417f06 Mon Sep 17 00:00:00 2001 From: Amken USA <166057890+amken3d@users.noreply.github.com> Date: Sat, 18 Jan 2025 01:49:00 -0500 Subject: [PATCH 053/137] Added TMC5160 support (#725) TMC5160: Added TMC5160 support * Added example code for tmc5160 and smoke test * Update go.mod * Updated mod file * Cleaned up commented out code and updated readme. * ran go fmt * Fixed setrampspeed func * Removed spi1 pin setup in example.go. Renamed SPIComm to spicomm.go * Removed unused test file * Removed commented line --- examples/tmc5160/main.go | 61 ++ go.mod | 9 +- go.sum | 8 +- smoketest.sh | 1 + tmc5160/README.MD | 211 ++++ tmc5160/address.go | 82 ++ tmc5160/helpers.go | 56 + tmc5160/registers.go | 2080 ++++++++++++++++++++++++++++++++++++++ tmc5160/spicomm.go | 134 +++ tmc5160/stepper.go | 105 ++ tmc5160/tmc5160.go | 194 ++++ tmc5160/uartcomm.go | 101 ++ tmc5160/utils.go | 13 + 13 files changed, 3050 insertions(+), 5 deletions(-) create mode 100644 examples/tmc5160/main.go create mode 100644 tmc5160/README.MD create mode 100644 tmc5160/address.go create mode 100644 tmc5160/helpers.go create mode 100644 tmc5160/registers.go create mode 100644 tmc5160/spicomm.go create mode 100644 tmc5160/stepper.go create mode 100644 tmc5160/tmc5160.go create mode 100644 tmc5160/uartcomm.go create mode 100644 tmc5160/utils.go diff --git a/examples/tmc5160/main.go b/examples/tmc5160/main.go new file mode 100644 index 0000000..309ea14 --- /dev/null +++ b/examples/tmc5160/main.go @@ -0,0 +1,61 @@ +// Connects to SPI1 on a RP2040 (Pico) +package main + +import ( + "machine" + + "tinygo.org/x/drivers/tmc5160" +) + +func main() { + // Step 1. Setup your protocol. SPI setup shown below + spi := machine.SPI1 + spi.Configure(machine.SPIConfig{ + Frequency: 12000000, // Upto 12 MHZ is pretty stable. Reduce to 5 or 6 Mhz if you are experiencing issues + Mode: 3, + LSBFirst: false, + }) + // Step 2. Set up all associated Pins + csPin0 := machine.GPIO13 + csPin0.Configure(machine.PinConfig{Mode: machine.PinOutput}) + enn0 := machine.GPIO18 + enn0.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + // csPins is a map of all chip select pins in a multi driver setup. + //Only one pin csPin0 mapped to "0"is shown in this example, but add more mappings as required + csPins := map[uint8]machine.Pin{0: csPin0} + //bind csPin to driverAdddress + driverAddress := uint8(0) // Let's assume we are working with driver at address 0x01 + // Step 3. Bind the communication interface to the protocol + comm := tmc5160.NewSPIComm(*spi, csPins) + // Step 4. Define your stepper like this below + //stepper := tmc5160.NewStepper(angle , gearRatio vSupply rCoil , lCoil , iPeak , rSense , mSteps, fclk ) + stepper := tmc5160.NewDefaultStepper() // Default Stepper should be used only for testing. + // Step 5. Instantiate your driver + driver := tmc5160.NewDriver( + comm, + driverAddress, + enn0, + stepper) + + // Setting and getting mode + rampMode := tmc5160.NewRAMPMODE(comm, driverAddress) + err := rampMode.SetMode(tmc5160.PositioningMode) + if err != nil { + return + } + mode, err := rampMode.GetMode() + if err != nil { + println("Error getting mode:", err) + } else { + println("Current Mode:", mode) + } + + // Read GCONF register + GCONF := tmc5160.NewGCONF() + gconfVal, err := driver.ReadRegister(tmc5160.GCONF) + // Uppack the register to get all the bits and bytes of the register + GCONF.Unpack(gconfVal) + //E.g. MultiStepFlit is retrieved from the GCONF register + println("GCONF:MultiStepFlit:", GCONF.MultistepFilt) +} diff --git a/go.mod b/go.mod index 0fa3076..530f060 100644 --- a/go.mod +++ b/go.mod @@ -1,20 +1,23 @@ module tinygo.org/x/drivers -go 1.18 +go 1.22.1 + +toolchain go1.23.1 require ( github.com/eclipse/paho.mqtt.golang v1.2.0 github.com/frankban/quicktest v1.10.2 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/orsinium-labs/tinymath v1.1.0 github.com/soypat/natiu-mqtt v0.5.1 + golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d golang.org/x/net v0.7.0 tinygo.org/x/tinyfont v0.3.0 tinygo.org/x/tinyterm v0.1.0 ) require ( - github.com/google/go-cmp v0.5.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/kr/pretty v0.2.1 // indirect github.com/kr/text v0.1.0 // indirect - golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect ) diff --git a/go.sum b/go.sum index 7f866dd..0d18cdf 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,9 @@ github.com/eclipse/paho.mqtt.golang v1.2.0 h1:1F8mhG9+aO5/xpdtFkW4SxOJB67ukuDC3t github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= github.com/frankban/quicktest v1.10.2 h1:19ARM85nVi4xH7xPXuc5eM/udya5ieh7b/Sv+d844Tk= github.com/frankban/quicktest v1.10.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= @@ -12,12 +13,15 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/orsinium-labs/tinymath v1.1.0 h1:KomdsyLHB7vE3f1nRAJF2dyf1m/gnM2HxfTeV1vS5UA= +github.com/orsinium-labs/tinymath v1.1.0/go.mod h1:WPXX6ei3KSXG7JfA03a+ekCYaY9SWN4I+JRl2p6ck+A= github.com/soypat/natiu-mqtt v0.5.1 h1:rwaDmlvjzD2+3MCOjMZc4QEkDkNwDzbct2TJbpz+TPc= github.com/soypat/natiu-mqtt v0.5.1/go.mod h1:xEta+cwop9izVCW7xOx2W+ct9PRMqr0gNVkvBPnQTc4= github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= +golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= tinygo.org/x/drivers v0.14.0/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= tinygo.org/x/drivers v0.15.1/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= diff --git a/smoketest.sh b/smoketest.sh index c89ba04..92b031d 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -136,6 +136,7 @@ tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mpu9150/mai tinygo build -size short -o ./build/test.hex -target=macropad-rp2040 ./examples/sh1106/macropad_spi tinygo build -size short -o ./build/test.hex -target=macropad-rp2040 ./examples/encoders/quadrature-interrupt tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mcp9808/main.go +tinygo build -size short -o ./build/test.hex -target=pico ./examples/tmc5160/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/tmc5160/README.MD b/tmc5160/README.MD new file mode 100644 index 0000000..b0b4916 --- /dev/null +++ b/tmc5160/README.MD @@ -0,0 +1,211 @@ +# TMC5160 Driver for Go (TinyGo) + +This repository provides a Go-based driver for the **TMC5160** stepper motor driver, implemented for both **SPI** and **UART** communication modes. The driver allows you to easily interface with the TMC5160 to configure and control stepper motors. + +## Table of Contents + +- [Installation](#installation) +- [Communication Modes](#communication-modes) + - [SPI Mode](#spi-mode) + - [UART Mode](#uart-mode) +- [Usage Example](#usage-example) + - [Setting and Getting Modes](#setting-and-getting-modes) + - [Reading and Writing Registers](#reading-and-writing-registers) +- [API Reference](#api-reference) +- [License](#license) + +## Installation + +To use the TMC5160 driver, you'll need to have **TinyGo** installed. You can install TinyGo by following the [official installation guide](https://tinygo.org/getting-started/). + +### Dependencies + +- **machine**: To interface with hardware on platforms like Raspberry Pi, STM32, etc. +- **TinyGo**: A Go compiler for embedded systems. + +Add the module + +```bash +import "tinygo.org/x/drivers/tmc5160" +``` + +### Communication Modes + +The TMC5160 supports two communication modes for controlling the motor: + +**SPI Mode** + +To communicate with the TMC5160 in SPI mode, you'll need to configure the SPI bus and the chip-select (CS) pin. This allows full-speed communication between your microcontroller and the TMC5160. +SPI Setup + +In SPI mode, you must configure the SPI interface on your microcontroller. Here's how to set up SPI communication for the TMC5160. + +```go +spi := machine.SPI1 +csPin := machine.GPIO13 +spi.Configure(machine.SPIConfig{ +SCK: machine.GPIO10, +SDI: machine.GPIO11, +SDO: machine.GPIO12, +Frequency: 5000000, +Mode: 3, +LSBFirst: false, +}) + +csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) +``` +**Sending Commands via SPI** + +The driver supports reading and writing registers using the SPIComm interface, which is initialized with the configured SPI bus and CS pins + +```go +comm := tmc5160.NewSPIComm(*spi, csPins) +driver := tmc5160.NewTMC5160(comm, driverIndex) +driver.WriteRegister(tmc5160.GCONF, value) + +``` + +**UART Mode** + +Alternatively, you can use UART mode to communicate with the TMC5160. UART mode is useful for cases where SPI is not available or when the TMC5160 is used in multi-driver configurations with limited SPI pins. +UART Setup + +In UART mode, you will need to configure the UART interface with the appropriate baud rate and settings: + +```go +uart := machine.UART0 +uart.Configure(machine.UARTConfig{ + BaudRate: 115200, +}) +``` +#### Sending Commands via UART + +The UART communication is handled through the UARTComm struct, which wraps the UART interface. + +```go +comm := tmc5160.NewUARTComm(uart, 0x01) +driver := tmc5160.NewTMC5160(comm, 0) +driver.WriteRegister(tmc5160.GCONF, 0x01) +``` + +## Usage Example + +Here’s a simple example of how to use the TMC5160 driver with SPI and UART modes: + +```aiignore +// Connects to SPI1 on a RP2040 (Pico) +package main + +import ( + "machine" + + "tinygo.org/x/drivers/tmc5160" +) + +func main() { + // Step 1. Setup your protocol. SPI setup shown below + spi := machine.SPI1 + spi.Configure(machine.SPIConfig{ + SCK: machine.GPIO10, + SDI: machine.GPIO11, + SDO: machine.GPIO12, + Frequency: 12000000, // Upto 12 MHZ is pretty stable. Reduce to 5 or 6 Mhz if you are experiencing issues + Mode: 3, + LSBFirst: false, + }) + // Step 2. Set up all associated Pins + csPin0 := machine.GPIO13 + csPin0.Configure(machine.PinConfig{Mode: machine.PinOutput}) + enn0 := machine.GPIO18 + enn0.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + // csPins is a map of all chip select pins in a multi driver setup. + //Only one pin csPin0 mapped to "0"is shown in this example, but add more mappings as required + csPins := map[uint8]machine.Pin{0: csPin0} + //bind csPin to driverAdddress + driverAddress := uint8(0) // Let's assume we are working with driver at address 0x01 + // Step 3. Bind the communication interface to the protocol + comm := tmc5160.NewSPIComm(*spi, csPins) + // Step 4. Define your stepper like this below + //stepper := tmc5160.NewStepper(angle , gearRatio vSupply rCoil , lCoil , iPeak , rSense , mSteps, fclk ) + stepper := tmc5160.NewDefaultStepper() // Default Stepper should be used only for testing. + // Step 5. Instantiate your driver + driver := tmc5160.NewDriver( + comm, + driverAddress, + enn0, + stepper) + + // Setting and getting mode + rampMode := tmc5160.NewRAMPMODE(comm, driverAddress) + err := rampMode.SetMode(tmc5160.PositioningMode) + if err != nil { + return + } + mode, err := rampMode.GetMode() + if err != nil { + println("Error getting mode:", err) + } else { + println("Current Mode:", mode) + } + + // Read GCONF register + GCONF := tmc5160.NewGCONF() + gconfVal, err := driver.ReadRegister(tmc5160.GCONF) + // Uppack the register to get all the bits and bytes of the register + GCONF.Unpack(gconfVal) + //E.g. MultiStepFlit is retrieved from the GCONF register + println("GCONF:MultiStepFlit:", GCONF.MultistepFilt) +} + + +``` +## Reading and Writing Registers + +You can easily read and write registers using the WriteRegister and ReadRegister methods: + +```aiignore +// Write a value to a register +err := driver.WriteRegister(tmc5160.GCONF, 0x01) +if err != nil { + fmt.Println("Error writing register:", err) +} + +// Read a register +value, err := driver.ReadRegister(tmc5160.GCONF) +if err != nil { + fmt.Println("Error reading register:", err) +} else { + fmt.Println("Read value from GCONF:", value) +} + +``` + +## API Reference + + NewSPIComm(spi machine.SPI, csPins map[uint8]machine.Pin) *SPIComm + +Creates a new SPI communication interface for the TMC5160. + + NewUARTComm(uart machine.UART, address uint8) *UARTComm + +Creates a new UART communication interface for the TMC5160. + + NewTMC5160(comm RegisterComm, address uint8) *TMC5160 + +Creates a new instance of the TMC5160 driver. + + WriteRegister(register uint8, value uint32) error + +Writes a value to the specified register. + + ReadRegister(register uint8) (uint32, error) + +Reads a value from the specified register. + +## License + +This project is licensed under the MIT License + + + diff --git a/tmc5160/address.go b/tmc5160/address.go new file mode 100644 index 0000000..473626c --- /dev/null +++ b/tmc5160/address.go @@ -0,0 +1,82 @@ +package tmc5160 + +// Driver Register addresses +const ( + GCONF uint8 = 0x00 // Global configuration flags + GSTAT uint8 = 0x01 // Global status flags + IFCNT = 0x02 // UART transmission counter + SLAVECONF = 0x03 // UART slave configuration + IOIN = 0x04 // Read input / write output pins + X_COMPARE = 0x05 // Position comparison register + OTP_PROG = 0x06 // OTP programming register + OTP_READ = 0x07 // OTP read register + FACTORY_CONF = 0x08 // Factory configuration (clock trim) + SHORT_CONF = 0x09 // Short detector configuration + DRV_CONF = 0x0A // Driver configuration + GLOBAL_SCALER = 0x0B // Global scaling of motor current + OFFSET_READ = 0x0C // Offset calibration results + + /* Velocity dependent driver feature control registers */ + IHOLD_IRUN = 0x10 // Driver current control + TPOWERDOWN = 0x11 // Delay before power down + TSTEP = 0x12 // Actual time between microsteps + TPWMTHRS = 0x13 // Upper velocity for stealthChop voltage PWM mode + TCOOLTHRS = 0x14 // Lower threshold velocity for switching on smart energy coolStep and stallGuard feature + THIGH = 0x15 // Velocity threshold for switching into a different chopper mode and fullstepping + + /* Ramp generator motion control registers */ + RAMPMODE = 0x20 // Driving mode (Velocity, Positioning, Hold) + XACTUAL = 0x21 // Actual motor position + VACTUAL = 0x22 // Actual motor velocity from ramp generator + VSTART = 0x23 // Motor start velocity + A_1 = 0x24 // First acceleration between VSTART and V1 + V_1 = 0x25 // First acceleration/deceleration phase target velocity + AMAX = 0x26 // Second acceleration between V1 and VMAX + VMAX = 0x27 // Target velocity in velocity mode + DMAX = 0x28 // Deceleration between VMAX and V1 + D_1 = 0x2A // Deceleration between V1 and VSTOP + //Attention: Do not set 0 in positioning mode, even if V1=0! + VSTOP = 0x2B // Motor stop velocity + //Attention: Set VSTOP > VSTART! + //Attention: Do not set 0 in positioning mode, minimum 10 recommend! + TZEROWAIT = 0x2C // Waiting time after ramping down to zero velocity before next movement or direction inversion can start. + XTARGET = 0x2D // Target position for ramp mode + + /* Ramp generator driver feature control registers */ + VDCMIN = 0x33 // Velocity threshold for enabling automatic commutation dcStep + SW_MODE = 0x34 // Switch mode configuration + RAMP_STAT = 0x35 // Ramp status and switch event status + XLATCH = 0x36 // Ramp generator latch position upon programmable switch event + + /* Encoder registers */ + ENCMODE = 0x38 // Encoder configuration and use of N channel + X_ENC = 0x39 // Actual encoder position + ENC_CONST = 0x3A // Accumulation constant + ENC_STATUS = 0x3B // Encoder status information + ENC_LATCH = 0x3C // Encoder position latched on N event + ENC_DEVIATION = 0x3D // Maximum number of steps deviation between encoder counter and XACTUAL for deviation warning + + /* Motor driver registers */ + MSLUT0 uint8 = 0x60 // 32 bits + MSLUT1 uint8 = 0x61 // 32 bits + MSLUT2 uint8 = 0x62 // 32 bits + MSLUT3 uint8 = 0x63 // 32 bits + MSLUT4 uint8 = 0x64 // 32 bits + MSLUT5 uint8 = 0x65 // 32 bits + MSLUT6 uint8 = 0x66 // 32 bits + MSLUT7 uint8 = 0x67 // 32 bits + MSLUTSEL = 0x68 // Look up table segmentation definition + MSLUTSTART = 0x69 // Absolute current at microstep table entries 0 and 256 + MSCNT = 0x6A // Actual position in the microstep table + MSCURACT = 0x6B // Actual microstep current + CHOPCONF = 0x6C // Chopper and driver configuration + COOLCONF = 0x6D // coolStep smart current control register and stallGuard2 configuration + DCCTRL = 0x6E // dcStep automatic commutation configuration register + DRV_STATUS = 0x6F // stallGuard2 value and driver error flags + PWMCONF = 0x70 // stealthChop voltage PWM mode chopper configuration + PWM_SCALE = 0x71 // Results of stealthChop amplitude regulator. + PWM_AUTO = 0x72 // Automatically determined PWM config values + LOST_STEPS = 0x73 // Number of input steps skipped due to dcStep. only with SD_MODE = 1 + expectedVersion = 0x03 + DEFAULT_F_CLK = 12000000 +) diff --git a/tmc5160/helpers.go b/tmc5160/helpers.go new file mode 100644 index 0000000..3ffa1da --- /dev/null +++ b/tmc5160/helpers.go @@ -0,0 +1,56 @@ +package tmc5160 + +import ( + "github.com/orsinium-labs/tinymath" + "golang.org/x/exp/constraints" +) + +// VelocityToVMAX calculates the VMAX register value from the current stepper velocity which is in microsteps per tRef (i.e 1/clock speed) +func (stepper *Stepper) CurrentVelocityToVMAX() uint32 { + tref := float32(16777216) / (float32(stepper.Fclk) * 1000000) + r := stepper.VelocitySPS * stepper.GearRatio * float32(tref) + return constrain(uint32(r), 0, maxVMAX) // VMAX register value cannot exceed maxVMAX +} +func (stepper *Stepper) DesiredVelocityToVMAX(v float32) uint32 { + tref := 16777216 / (float32(stepper.Fclk) * 1000000) + r := tinymath.Round(v * stepper.GearRatio * tref) + return constrain(uint32(r), 0, maxVMAX) // VMAX register value cannot exceed maxVMAX +} + +func (stepper *Stepper) DesiredAccelToAMAX(dacc float32, dVel float32) uint32 { + dVelToVMAX := stepper.DesiredVelocityToVMAX(dVel) + _a := uint64(dVelToVMAX) * 131072 + _b := float32(_a) / dacc + _c := _b / float32(uint32(stepper.Fclk)*1000000) + return uint32(_c) + +} + +// Convert threshold speed (Hz) to internal TSTEP value +func (stepper *Stepper) DesiredSpeedToTSTEP(thrsSpeed uint32) uint32 { + if thrsSpeed < 0 { + return 0 + } + _a := stepper.DesiredVelocityToVMAX(float32(thrsSpeed)) + _b := float32(16777216 / _a) + _c := float32(stepper.MSteps) / float32(256) + _d := uint32(_b * _c) + return constrain(_d, 0, 1048575) +} + +func (stepper *Stepper) VMAXToTSTEP(vmax uint32) uint32 { + _b := float32(16777216 / vmax) + _c := float32(stepper.MSteps) / float32(256) + _d := tinymath.Round(_b * _c) + return constrain(uint32(_d), 0, 1048575) +} + +// Constrain function to limit values to a specific range (supports multiple types). +func constrain[T constraints.Ordered](value, min, max T) T { + if value < min { + return min + } else if value > max { + return max + } + return value +} diff --git a/tmc5160/registers.go b/tmc5160/registers.go new file mode 100644 index 0000000..d4d13a1 --- /dev/null +++ b/tmc5160/registers.go @@ -0,0 +1,2080 @@ +package tmc5160 + +import ( + math "github.com/orsinium-labs/tinymath" +) + +// RegisterComm defines an interface for reading from and writing to hardware registers. +type RegisterComm interface { + ReadRegister(register uint8, driverIndex uint8) (uint32, error) + WriteRegister(register uint8, value uint32, driverIndex uint8) error +} + +// ReadRegister function using the register constants +func ReadRegister(comm RegisterComm, driverIndex uint8, register uint8) (uint32, error) { + // Read the register value using the comm interface + + value, err := comm.ReadRegister(register, driverIndex) + if err != nil { + return 0, err + } + return value, nil +} + +// WriteRegister function using the register constants +func WriteRegister(comm RegisterComm, register uint8, driverIndex uint8, value uint32) error { + // Write the value to the register using the comm interface + return comm.WriteRegister(register, value, driverIndex) +} + +// Register and methods to pack and unpack +// Base Register struct +type Register struct { + RegisterAddr uint8 + Bytes uint32 +} + +// Common New function for creating a new register instance +func NewRegister(addr uint8) *Register { + return &Register{ + RegisterAddr: addr, + } +} + +// Common Pack method: for subclasses to implement their packing logic +func (r *Register) Pack() uint32 { + return r.Bytes // Default, should be overridden in register-specific structs +} + +// Common Unpack method: for subclasses to implement their unpacking logic +func (r *Register) Unpack(registerValue uint32) { + r.Bytes = registerValue // Default, should be overridden in register-specific structs +} + +// Common GetAddress method +func (r *Register) GetAddress() uint8 { + return r.RegisterAddr +} + +// Common Read method (assuming the communication interface is implemented) +func (r *Register) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, r.RegisterAddr) +} + +// Common Write method +func (r *Register) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, r.RegisterAddr, driverIndex, value) +} + +// GCONF Register bit fields' masks and shifts +const ( + // Recalibrate: Zero crossing recalibration during driver disable + GCONF_Recalibrate_Mask = 1 << 0 + // Faststandstill: Timeout for step execution until standstill detection + GCONF_Faststandstill_Mask = 1 << 1 + // Enable PWM mode for StealthChop + GCONF_EnPwmMode_Mask = 1 << 2 + // Enable step input filtering for StealthChop optimization + GCONF_MultistepFilt_Mask = 1 << 3 + // Motor direction + GCONF_Shaft_Mask = 1 << 4 + // Error flags on DIAG0 pin + GCONF_Diag0Error_Mask = 1 << 5 + // Enable DIAG0 for Over temperature warning + GCONF_Diag0Otpw_Mask = 1 << 6 + // Enable DIAG0 for stall step detection + GCONF_Diag0StallStep_Mask = 1 << 7 + // Enable DIAG1 for stall direction + GCONF_Diag1StallDir_Mask = 1 << 8 + // Enable DIAG1 for index position + GCONF_Diag1Index_Mask = 1 << 9 + // Enable DIAG1 for chopper on state + GCONF_Diag1Onstate_Mask = 1 << 10 + // Enable DIAG1 for skipped steps + GCONF_Diag1StepsSkipped_Mask = 1 << 11 + // Enable DIAG0 push-pull output + GCONF_Diag0IntPushPull_Mask = 1 << 12 + // Enable DIAG1 push-pull output + GCONF_Diag1PosCompPushPull_Mask = 1 << 13 + // Small hysteresis for step frequency comparison + GCONF_SmallHysteresis_Mask = 1 << 14 + // Enable emergency stop + GCONF_StopEnable_Mask = 1 << 15 + // Direct motor coil current and polarity control + GCONF_DirectMode_Mask = 1 << 16 + // Test mode (not for normal use) + GCONF_TestMode_Mask = 1 << 17 +) + +// GCONF Register structure +type GCONF_Register struct { + Register + // Fields corresponding to individual settings in GCONF register + Recalibrate bool + Faststandstill bool + EnPwmMode bool + MultistepFilt bool + Shaft bool + Diag0Error bool + Diag0Otpw bool + Diag0StallStep bool + Diag1StallDir bool + Diag1Index bool + Diag1Onstate bool + Diag1StepsSkipped bool + Diag0IntPushPull bool + Diag1PosCompPushPull bool + SmallHysteresis bool + StopEnable bool + DirectMode bool + TestMode bool +} + +// NewGCONF initializes a new GCONF register instance +func NewGCONF() *GCONF_Register { + return &GCONF_Register{ + Register: Register{ + RegisterAddr: GCONF, // GSTAT register address + }, + } +} + +// Pack the fields into a single 32-bit register value +func (g *GCONF_Register) Pack() uint32 { + var registerValue uint32 + + // Use bitwise OR to set individual bits based on the field values + if g.Recalibrate { + registerValue |= GCONF_Recalibrate_Mask + } + if g.Faststandstill { + registerValue |= GCONF_Faststandstill_Mask + } + if g.EnPwmMode { + registerValue |= GCONF_EnPwmMode_Mask + } + if g.MultistepFilt { + registerValue |= GCONF_MultistepFilt_Mask + } + if g.Shaft { + registerValue |= GCONF_Shaft_Mask + } + if g.Diag0Error { + registerValue |= GCONF_Diag0Error_Mask + } + if g.Diag0Otpw { + registerValue |= GCONF_Diag0Otpw_Mask + } + if g.Diag0StallStep { + registerValue |= GCONF_Diag0StallStep_Mask + } + if g.Diag1StallDir { + registerValue |= GCONF_Diag1StallDir_Mask + } + if g.Diag1Index { + registerValue |= GCONF_Diag1Index_Mask + } + if g.Diag1Onstate { + registerValue |= GCONF_Diag1Onstate_Mask + } + if g.Diag1StepsSkipped { + registerValue |= GCONF_Diag1StepsSkipped_Mask + } + if g.Diag0IntPushPull { + registerValue |= GCONF_Diag0IntPushPull_Mask + } + if g.Diag1PosCompPushPull { + registerValue |= GCONF_Diag1PosCompPushPull_Mask + } + if g.SmallHysteresis { + registerValue |= GCONF_SmallHysteresis_Mask + } + if g.StopEnable { + registerValue |= GCONF_StopEnable_Mask + } + if g.DirectMode { + registerValue |= GCONF_DirectMode_Mask + } + if g.TestMode { + registerValue |= GCONF_TestMode_Mask + } + return registerValue +} + +// Unpack a 32-bit register value into individual fields +func (g *GCONF_Register) Unpack(registerValue uint32) { + g.Recalibrate = (registerValue & GCONF_Recalibrate_Mask) != 0 + g.Faststandstill = (registerValue & GCONF_Faststandstill_Mask) != 0 + g.EnPwmMode = (registerValue & GCONF_EnPwmMode_Mask) != 0 + g.MultistepFilt = (registerValue & GCONF_MultistepFilt_Mask) != 0 + g.Shaft = (registerValue & GCONF_Shaft_Mask) != 0 + g.Diag0Error = (registerValue & GCONF_Diag0Error_Mask) != 0 + g.Diag0Otpw = (registerValue & GCONF_Diag0Otpw_Mask) != 0 + g.Diag0StallStep = (registerValue & GCONF_Diag0StallStep_Mask) != 0 + g.Diag1StallDir = (registerValue & GCONF_Diag1StallDir_Mask) != 0 + g.Diag1Index = (registerValue & GCONF_Diag1Index_Mask) != 0 + g.Diag1Onstate = (registerValue & GCONF_Diag1Onstate_Mask) != 0 + g.Diag1StepsSkipped = (registerValue & GCONF_Diag1StepsSkipped_Mask) != 0 + g.Diag0IntPushPull = (registerValue & GCONF_Diag0IntPushPull_Mask) != 0 + g.Diag1PosCompPushPull = (registerValue & GCONF_Diag1PosCompPushPull_Mask) != 0 + g.SmallHysteresis = (registerValue & GCONF_SmallHysteresis_Mask) != 0 + g.StopEnable = (registerValue & GCONF_StopEnable_Mask) != 0 + g.DirectMode = (registerValue & GCONF_DirectMode_Mask) != 0 + g.TestMode = (registerValue & GCONF_TestMode_Mask) != 0 +} + +// Example Register: GSTAT +type GSTAT_Register struct { + Register + Reset bool + DrvErr bool + UvCp bool +} + +// NewGSTAT creates a new GSTAT register instance +func NewGSTAT() *GSTAT_Register { + return &GSTAT_Register{ + Register: Register{ + RegisterAddr: GSTAT, // GSTAT register address + }, + } +} + +// Pack method for GSTAT: overrides the base Pack +func (g *GSTAT_Register) Pack() uint32 { + var registerValue uint32 + if g.Reset { + registerValue |= 1 << 0 + } + if g.DrvErr { + registerValue |= 1 << 1 + } + if g.UvCp { + registerValue |= 1 << 2 + } + return registerValue +} + +// Unpack method for GSTAT: overrides the base Unpack +func (g *GSTAT_Register) Unpack(registerValue uint32) { + g.Reset = (registerValue & (1 << 0)) != 0 + g.DrvErr = (registerValue & (1 << 1)) != 0 + g.UvCp = (registerValue & (1 << 2)) != 0 +} + +// IOIN_Register struct to represent the IOIN register +type IOIN_Register struct { + Register + ReflStep bool + RefrDir bool + EncbDcenCfg4 bool + EncaDcinCfg5 bool + DrvEnn bool + EncNDcoCfg6 bool + SdMode bool + SwcompIn bool + Version uint8 +} + +// NewIOIN creates a new IOIN register instance +func NewIOIN() *IOIN_Register { + return &IOIN_Register{ + Register: Register{ + RegisterAddr: IOIN, + }, + } +} + +// Pack method for IOIN: overrides the base Pack +func (i *IOIN_Register) Pack() uint32 { + var registerValue uint32 + + // Set individual bits based on the field values + if i.ReflStep { + registerValue |= 1 << 0 + } + if i.RefrDir { + registerValue |= 1 << 1 + } + if i.EncbDcenCfg4 { + registerValue |= 1 << 2 + } + if i.EncaDcinCfg5 { + registerValue |= 1 << 3 + } + if i.DrvEnn { + registerValue |= 1 << 4 + } + if i.EncNDcoCfg6 { + registerValue |= 1 << 5 + } + if i.SdMode { + registerValue |= 1 << 6 + } + if i.SwcompIn { + registerValue |= 1 << 7 + } + // Handle the version field (8 bits, starting at bit 24) + registerValue |= uint32(i.Version) << 24 + + return registerValue +} + +// Unpack method for IOIN: overrides the base Unpack +func (i *IOIN_Register) Unpack(registerValue uint32) { + i.ReflStep = (registerValue & (1 << 0)) != 0 + i.RefrDir = (registerValue & (1 << 1)) != 0 + i.EncbDcenCfg4 = (registerValue & (1 << 2)) != 0 + i.EncaDcinCfg5 = (registerValue & (1 << 3)) != 0 + i.DrvEnn = (registerValue & (1 << 4)) != 0 + i.EncNDcoCfg6 = (registerValue & (1 << 5)) != 0 + i.SdMode = (registerValue & (1 << 6)) != 0 + i.SwcompIn = (registerValue & (1 << 7)) != 0 + // Extract the version field (8 bits, starting at bit 24) + i.Version = uint8((registerValue >> 24) & 0xFF) +} + +// SHORT_CONF_Register struct to represent the SHORT_CONF register +type SHORT_CONF_Register struct { + Register + S2vsLevel uint8 // Short to VS detector sensitivity (4 bits) + S2gLevel uint8 // Short to GND detector sensitivity (4 bits) + ShortFilter uint8 // Spike filtering bandwidth for short detection (2 bits) + ShortDelay bool // Short detection delay (1 bit) +} + +// NewSHORT_CONF creates a new SHORT_CONF register instance +func NewSHORT_CONF() *SHORT_CONF_Register { + return &SHORT_CONF_Register{ + Register: Register{ + RegisterAddr: SHORT_CONF, + }, + } +} + +// Pack method for SHORT_CONF: overrides the base Pack +func (s *SHORT_CONF_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(s.S2vsLevel&0xF) << 0 // S2vsLevel: 4 bits + registerValue |= uint32(s.S2gLevel&0xF) << 8 // S2gLevel: 4 bits + registerValue |= uint32(s.ShortFilter&0x3) << 16 // ShortFilter: 2 bits + if s.ShortDelay { + registerValue |= 1 << 18 // ShortDelay: 1 bit + } + return registerValue +} + +// Unpack method for SHORT_CONF: overrides the base Unpack +func (s *SHORT_CONF_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + s.S2vsLevel = uint8((registerValue >> 0) & 0xF) // Extract 4 bits for S2vsLevel + s.S2gLevel = uint8((registerValue >> 8) & 0xF) // Extract 4 bits for S2gLevel + s.ShortFilter = uint8((registerValue >> 16) & 0x3) // Extract 2 bits for ShortFilter + s.ShortDelay = (registerValue & (1 << 18)) != 0 // Extract 1 bit for ShortDelay +} + +// DRV_CONF_Register struct to represent the DRV_CONF register +type DRV_CONF_Register struct { + Register + BBMTime uint8 // Break before make delay (5 bits) + BBMClks uint8 // Digital BBM Time in clock cycles (4 bits) + OTSelect uint8 // Over temperature level selection for bridge disable (2 bits) + DrvStrength uint8 // Gate drivers current selection (2 bits) + FiltIsense uint8 // Filter time constant of sense amplifier (2 bits) +} + +// NewDRV_CONF creates a new DRV_CONF register instance +func NewDRV_CONF() *DRV_CONF_Register { + return &DRV_CONF_Register{ + Register: Register{ + RegisterAddr: DRV_CONF, + }, + } +} + +// Pack method for DRV_CONF: overrides the base Pack +func (d *DRV_CONF_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(d.BBMTime&0x1F) << 0 // BBMTime: 5 bits + registerValue |= uint32(d.BBMClks&0xF) << 8 // BBMClks: 4 bits + registerValue |= uint32(d.OTSelect&0x3) << 16 // OTSelect: 2 bits + registerValue |= uint32(d.DrvStrength&0x3) << 18 // DrvStrength: 2 bits + registerValue |= uint32(d.FiltIsense&0x3) << 20 // FiltIsense: 2 bits + + return registerValue +} + +// Unpack method for DRV_CONF: overrides the base Unpack +func (d *DRV_CONF_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + d.BBMTime = uint8((registerValue >> 0) & 0x1F) // Extract 5 bits for BBMTime + d.BBMClks = uint8((registerValue >> 8) & 0xF) // Extract 4 bits for BBMClks + d.OTSelect = uint8((registerValue >> 16) & 0x3) // Extract 2 bits for OTSelect + d.DrvStrength = uint8((registerValue >> 18) & 0x3) // Extract 2 bits for DrvStrength + d.FiltIsense = uint8((registerValue >> 20) & 0x3) // Extract 2 bits for FiltIsense +} + +// OFFSET_READ_Register struct to represent the OFFSET_READ register +type OFFSET_READ_Register struct { + Register + PhaseB uint8 // Phase B offset (8 bits) + PhaseA uint8 // Phase A offset (8 bits) +} + +// NewOFFSET_READ creates a new OFFSET_READ register instance +func NewOFFSET_READ() *OFFSET_READ_Register { + return &OFFSET_READ_Register{ + Register: Register{ + RegisterAddr: OFFSET_READ, + }, + } +} + +// Pack method for OFFSET_READ: overrides the base Pack +func (o *OFFSET_READ_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(o.PhaseB&0xFF) << 0 // PhaseB: 8 bits + registerValue |= uint32(o.PhaseA&0xFF) << 8 // PhaseA: 8 bits + + return registerValue +} + +// Unpack method for OFFSET_READ: overrides the base Unpack +func (o *OFFSET_READ_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + o.PhaseB = uint8((registerValue >> 0) & 0xFF) // Extract 8 bits for PhaseB + o.PhaseA = uint8((registerValue >> 8) & 0xFF) // Extract 8 bits for PhaseA +} + +// IHOLD_IRUN_Register struct to represent the IHOLD_IRUN register +type IHOLD_IRUN_Register struct { + Register + Ihold uint8 // Standstill current (5 bits) + Irun uint8 // Motor run current (5 bits) + IholdDelay uint8 // Motor power down delay (4 bits) +} + +// NewIHOLD_IRUN creates a new IHOLD_IRUN register instance +func NewIHOLD_IRUN() *IHOLD_IRUN_Register { + return &IHOLD_IRUN_Register{ + Register: Register{ + RegisterAddr: IHOLD_IRUN, + }, + } +} + +// Pack method for IHOLD_IRUN: overrides the base Pack +func (i *IHOLD_IRUN_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(i.Ihold&0x1F) << 0 // Ihold: 5 bits + registerValue |= uint32(i.Irun&0x1F) << 8 // Irun: 5 bits + registerValue |= uint32(i.IholdDelay&0xF) << 16 // IholdDelay: 4 bits + + return registerValue +} + +// Unpack method for IHOLD_IRUN: overrides the base Unpack +func (i *IHOLD_IRUN_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + i.Ihold = uint8((registerValue >> 0) & 0x1F) // Extract 5 bits for Ihold + i.Irun = uint8((registerValue >> 8) & 0x1F) // Extract 5 bits for Irun + i.IholdDelay = uint8((registerValue >> 16) & 0xF) // Extract 4 bits for IholdDelay +} + +// SW_MODE_Register struct to represent the SW_MODE register +type SW_MODE_Register struct { + Register + StopLEnable bool // Enable automatic motor stop during active left reference switch input + StopREnable bool // Enable automatic motor stop during active right reference switch input + PolStopL bool // Sets the active polarity of the left reference switch input + PolStopR bool // Sets the active polarity of the right reference switch input + SwapLR bool // Swap the left and right reference switch inputs + LatchLActive bool // Activate latching of the position to XLATCH upon an active going edge on REFL + LatchLInactive bool // Activate latching of the position to XLATCH upon an inactive going edge on REFL + LatchRActive bool // Activate latching of the position to XLATCH upon an active going edge on REFR + LatchRInactive bool // Activate latching of the position to XLATCH upon an inactive going edge on REFR + EnLatchEncoder bool // Latch encoder position to ENC_LATCH upon reference switch event + SgStop bool // Enable stop by stallGuard2 + EnSoftStop bool // Enable soft stop upon a stop event +} + +// NewSW_MODE creates a new SW_MODE register instance +func NewSW_MODE() *SW_MODE_Register { + return &SW_MODE_Register{ + Register: Register{ + RegisterAddr: SW_MODE, + }, + } +} + +// Pack method for SW_MODE: overrides the base Pack +func (s *SW_MODE_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + if s.StopLEnable { + registerValue |= 1 << 0 + } + if s.StopREnable { + registerValue |= 1 << 1 + } + if s.PolStopL { + registerValue |= 1 << 2 + } + if s.PolStopR { + registerValue |= 1 << 3 + } + if s.SwapLR { + registerValue |= 1 << 4 + } + if s.LatchLActive { + registerValue |= 1 << 5 + } + if s.LatchLInactive { + registerValue |= 1 << 6 + } + if s.LatchRActive { + registerValue |= 1 << 7 + } + if s.LatchRInactive { + registerValue |= 1 << 8 + } + if s.EnLatchEncoder { + registerValue |= 1 << 9 + } + if s.SgStop { + registerValue |= 1 << 10 + } + if s.EnSoftStop { + registerValue |= 1 << 11 + } + + return registerValue +} + +// Unpack method for SW_MODE: overrides the base Unpack +func (s *SW_MODE_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + s.StopLEnable = (registerValue & (1 << 0)) != 0 + s.StopREnable = (registerValue & (1 << 1)) != 0 + s.PolStopL = (registerValue & (1 << 2)) != 0 + s.PolStopR = (registerValue & (1 << 3)) != 0 + s.SwapLR = (registerValue & (1 << 4)) != 0 + s.LatchLActive = (registerValue & (1 << 5)) != 0 + s.LatchLInactive = (registerValue & (1 << 6)) != 0 + s.LatchRActive = (registerValue & (1 << 7)) != 0 + s.LatchRInactive = (registerValue & (1 << 8)) != 0 + s.EnLatchEncoder = (registerValue & (1 << 9)) != 0 + s.SgStop = (registerValue & (1 << 10)) != 0 + s.EnSoftStop = (registerValue & (1 << 11)) != 0 +} + +// RAMP_STAT_Register struct to represent the RAMP_STAT register +type RAMP_STAT_Register struct { + Register + StatusStopL bool // Reference switch left status (1=active) + StatusStopR bool // Reference switch right status (1=active) + StatusLatchL bool // Latch left ready (enable position latching) + StatusLatchR bool // Latch right ready (enable position latching) + EventStopL bool // Active stop left condition due to stop switch + EventStopR bool // Active stop right condition due to stop switch + EventStopSG bool // Active StallGuard2 stop event + EventPosReached bool // Target position reached + VelocityReached bool // Target velocity reached + PositionReached bool // Target position reached + VZero bool // Actual velocity is 0 + TZeroWaitActive bool // TZEROWAIT is active after motor stop + SecondMove bool // Automatic ramp required moving back in opposite direction + StatusSG bool // Active stallGuard2 input +} + +// NewRAMP_STAT creates a new RAMP_STAT register instance +func NewRAMP_STAT() *RAMP_STAT_Register { + return &RAMP_STAT_Register{ + Register: Register{ + RegisterAddr: RAMP_STAT, + }, + } +} + +// Pack method for RAMP_STAT: overrides the base Pack +func (r *RAMP_STAT_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + if r.StatusStopL { + registerValue |= 1 << 0 + } + if r.StatusStopR { + registerValue |= 1 << 1 + } + if r.StatusLatchL { + registerValue |= 1 << 2 + } + if r.StatusLatchR { + registerValue |= 1 << 3 + } + if r.EventStopL { + registerValue |= 1 << 4 + } + if r.EventStopR { + registerValue |= 1 << 5 + } + if r.EventStopSG { + registerValue |= 1 << 6 + } + if r.EventPosReached { + registerValue |= 1 << 7 + } + if r.VelocityReached { + registerValue |= 1 << 8 + } + if r.PositionReached { + registerValue |= 1 << 9 + } + if r.VZero { + registerValue |= 1 << 10 + } + if r.TZeroWaitActive { + registerValue |= 1 << 11 + } + if r.SecondMove { + registerValue |= 1 << 12 + } + if r.StatusSG { + registerValue |= 1 << 13 + } + + return registerValue +} + +// Unpack method for RAMP_STAT: overrides the base Unpack +func (r *RAMP_STAT_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + r.StatusStopL = (registerValue & (1 << 0)) != 0 + r.StatusStopR = (registerValue & (1 << 1)) != 0 + r.StatusLatchL = (registerValue & (1 << 2)) != 0 + r.StatusLatchR = (registerValue & (1 << 3)) != 0 + r.EventStopL = (registerValue & (1 << 4)) != 0 + r.EventStopR = (registerValue & (1 << 5)) != 0 + r.EventStopSG = (registerValue & (1 << 6)) != 0 + r.EventPosReached = (registerValue & (1 << 7)) != 0 + r.VelocityReached = (registerValue & (1 << 8)) != 0 + r.PositionReached = (registerValue & (1 << 9)) != 0 + r.VZero = (registerValue & (1 << 10)) != 0 + r.TZeroWaitActive = (registerValue & (1 << 11)) != 0 + r.SecondMove = (registerValue & (1 << 12)) != 0 + r.StatusSG = (registerValue & (1 << 13)) != 0 +} + +// ENCMODE_Register struct to represent the ENCMODE register +type ENCMODE_Register struct { + Register + PolA bool // Required A polarity for an N channel event + PolB bool // Required B polarity for an N channel event + PolN bool // Defines active polarity of N (0=low active, 1=high active) + IgnoreAB bool // Ignore A and B polarity for N channel event + ClrCont bool // Always latch or latch and clear X_ENC upon an N event + ClrOnce bool // Latch or latch and clear X_ENC on the next N event + Sensitivity uint8 // N channel event sensitivity (2 bits) + ClrEncX bool // Clear encoder counter X_ENC upon N-event + LatchXAct bool // Also latch XACTUAL position together with X_ENC + EncSelDecimal bool // Encoder prescaler divisor binary mode (0) / decimal mode (1) +} + +// NewENCMODE creates a new ENCMODE register instance +func NewENCMODE() *ENCMODE_Register { + return &ENCMODE_Register{ + Register: Register{ + RegisterAddr: ENCMODE, + }, + } +} + +// Pack method for ENCMODE: overrides the base Pack +func (e *ENCMODE_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + if e.PolA { + registerValue |= 1 << 0 + } + if e.PolB { + registerValue |= 1 << 1 + } + if e.PolN { + registerValue |= 1 << 2 + } + if e.IgnoreAB { + registerValue |= 1 << 3 + } + if e.ClrCont { + registerValue |= 1 << 4 + } + if e.ClrOnce { + registerValue |= 1 << 5 + } + registerValue |= uint32(e.Sensitivity&0x3) << 6 // Sensitivity: 2 bits + if e.ClrEncX { + registerValue |= 1 << 8 + } + if e.LatchXAct { + registerValue |= 1 << 9 + } + if e.EncSelDecimal { + registerValue |= 1 << 10 + } + + return registerValue +} + +// Unpack method for ENCMODE: overrides the base Unpack +func (e *ENCMODE_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + e.PolA = (registerValue & (1 << 0)) != 0 + e.PolB = (registerValue & (1 << 1)) != 0 + e.PolN = (registerValue & (1 << 2)) != 0 + e.IgnoreAB = (registerValue & (1 << 3)) != 0 + e.ClrCont = (registerValue & (1 << 4)) != 0 + e.ClrOnce = (registerValue & (1 << 5)) != 0 + e.Sensitivity = uint8((registerValue >> 6) & 0x3) // Extract 2 bits for Sensitivity + e.ClrEncX = (registerValue & (1 << 8)) != 0 + e.LatchXAct = (registerValue & (1 << 9)) != 0 + e.EncSelDecimal = (registerValue & (1 << 10)) != 0 +} + +// ENC_STATUS_Register struct to represent the ENC_STATUS register +type ENC_STATUS_Register struct { + Register + NEvent bool // N event detected + DeviationWarn bool // Deviation between X_ACTUAL and X_ENC detected +} + +// NewENC_STATUS creates a new ENC_STATUS register instance +func NewENC_STATUS() *ENC_STATUS_Register { + return &ENC_STATUS_Register{ + Register: Register{ + RegisterAddr: ENC_STATUS, + }, + } +} + +// Pack method for ENC_STATUS: overrides the base Pack +func (e *ENC_STATUS_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + if e.NEvent { + registerValue |= 1 << 0 + } + if e.DeviationWarn { + registerValue |= 1 << 1 + } + + return registerValue +} + +// Unpack method for ENC_STATUS: overrides the base Unpack +func (e *ENC_STATUS_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + e.NEvent = (registerValue & (1 << 0)) != 0 + e.DeviationWarn = (registerValue & (1 << 1)) != 0 +} + +// CHOPCONF_Register struct to represent the CHOPCONF register +type CHOPCONF_Register struct { + Register + Toff uint8 // Off time setting (4 bits) + HstrtTfd uint8 // Hysteresis start value or fast decay time setting (3 bits) + HendOffset uint8 // Hysteresis low value or sine wave offset (4 bits) + Tfd3 bool // Fast decay time setting bit 3 + Disfdcc bool // Disable current comparator usage for fast decay termination + Rndtf bool // Enable random modulation of chopper TOFF time + Chm bool // Chopper mode (0=standard, 1=constant off time with fast decay) + Tbl uint8 // Comparator blank time select (2 bits) + Vsense bool // Select resistor voltage sensitivity (low or high) + Vhighfs bool // Enable fullstep switching when VHIGH is exceeded + Vhighchm bool // Enable switching to chm=1 and fd=0 when VHIGH is exceeded + Tpfd uint8 // Passive fast decay time (4 bits) + Mres uint8 // Microstep resolution (4 bits) + Intpol bool // Enable interpolation to 256 microsteps + Dedge bool // Enable double edge step pulses + Diss2g bool // Disable short to GND protection + Diss2vs bool // Disable short to supply protection +} + +// NewCHOPCONF creates a new CHOPCONF register instance +func NewCHOPCONF() *CHOPCONF_Register { + return &CHOPCONF_Register{ + Register: Register{ + RegisterAddr: CHOPCONF, + }, + } +} + +// Pack method for CHOPCONF: overrides the base Pack +func (c *CHOPCONF_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(c.Toff&0xF) << 0 // Toff: 4 bits + registerValue |= uint32(c.HstrtTfd&0x7) << 4 // HstrtTfd: 3 bits + registerValue |= uint32(c.HendOffset&0xF) << 7 // HendOffset: 4 bits + if c.Tfd3 { + registerValue |= 1 << 11 // Tfd3: 1 bit + } + if c.Disfdcc { + registerValue |= 1 << 12 // Disfdcc: 1 bit + } + if c.Rndtf { + registerValue |= 1 << 13 // Rndtf: 1 bit + } + if c.Chm { + registerValue |= 1 << 14 // Chm: 1 bit + } + registerValue |= uint32(c.Tbl&0x3) << 15 // Tbl: 2 bits + if c.Vsense { + registerValue |= 1 << 17 // Vsense: 1 bit + } + if c.Vhighfs { + registerValue |= 1 << 18 // Vhighfs: 1 bit + } + if c.Vhighchm { + registerValue |= 1 << 19 // Vhighchm: 1 bit + } + registerValue |= uint32(c.Tpfd&0xF) << 20 // Tpfd: 4 bits + registerValue |= uint32(c.Mres&0xF) << 24 // Mres: 4 bits + if c.Intpol { + registerValue |= 1 << 28 // Intpol: 1 bit + } + if c.Dedge { + registerValue |= 1 << 29 // Dedge: 1 bit + } + if c.Diss2g { + registerValue |= 1 << 30 // Diss2g: 1 bit + } + if c.Diss2vs { + registerValue |= 1 << 31 // Diss2vs: 1 bit + } + + return registerValue +} + +// Unpack method for CHOPCONF: overrides the base Unpack +func (c *CHOPCONF_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + c.Toff = uint8((registerValue >> 0) & 0xF) // Extract 4 bits for Toff + c.HstrtTfd = uint8((registerValue >> 4) & 0x7) // Extract 3 bits for HstrtTfd + c.HendOffset = uint8((registerValue >> 7) & 0xF) // Extract 4 bits for HendOffset + c.Tfd3 = (registerValue & (1 << 11)) != 0 // Extract 1 bit for Tfd3 + c.Disfdcc = (registerValue & (1 << 12)) != 0 // Extract 1 bit for Disfdcc + c.Rndtf = (registerValue & (1 << 13)) != 0 // Extract 1 bit for Rndtf + c.Chm = (registerValue & (1 << 14)) != 0 // Extract 1 bit for Chm + c.Tbl = uint8((registerValue >> 15) & 0x3) // Extract 2 bits for Tbl + c.Vsense = (registerValue & (1 << 17)) != 0 // Extract 1 bit for Vsense + c.Vhighfs = (registerValue & (1 << 18)) != 0 // Extract 1 bit for Vhighfs + c.Vhighchm = (registerValue & (1 << 19)) != 0 // Extract 1 bit for Vhighchm + c.Tpfd = uint8((registerValue >> 20) & 0xF) // Extract 4 bits for Tpfd + c.Mres = uint8((registerValue >> 24) & 0xF) // Extract 4 bits for Mres + c.Intpol = (registerValue & (1 << 28)) != 0 // Extract 1 bit for Intpol + c.Dedge = (registerValue & (1 << 29)) != 0 // Extract 1 bit for Dedge + c.Diss2g = (registerValue & (1 << 30)) != 0 // Extract 1 bit for Diss2g + c.Diss2vs = (registerValue & (1 << 31)) != 0 // Extract 1 bit for Diss2vs +} + +// COOLCONF_Register struct to represent the COOLCONF register +type COOLCONF_Register struct { + Register + Semin uint8 // Minimum stallGuard2 value for smart current control (4 bits) + Seup uint8 // Current increment step width (2 bits) + Semax uint8 // stallGuard2 hysteresis value for smart current control (4 bits) + Sedn uint8 // Current decrement step speed (2 bits) + Seimin bool // Minimum current for smart current control (1 bit) + Sgt uint8 // stallGuard2 threshold value (7 bits) + Sfilt bool // Enable stallGuard2 filter (1 bit) +} + +// NewCOOLCONF creates a new COOLCONF register instance +func NewCOOLCONF() *COOLCONF_Register { + return &COOLCONF_Register{ + Register: Register{ + RegisterAddr: COOLCONF, + }, + } +} + +// Pack method for COOLCONF: overrides the base Pack +func (c *COOLCONF_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(c.Semin&0xF) << 0 // Semin: 4 bits + registerValue |= uint32(c.Seup&0x3) << 5 // Seup: 2 bits + registerValue |= uint32(c.Semax&0xF) << 8 // Semax: 4 bits + registerValue |= uint32(c.Sedn&0x3) << 13 // Sedn: 2 bits + if c.Seimin { + registerValue |= 1 << 15 // Seimin: 1 bit + } + registerValue |= uint32(c.Sgt&0x7F) << 16 // Sgt: 7 bits + if c.Sfilt { + registerValue |= 1 << 24 // Sfilt: 1 bit + } + + return registerValue +} + +// Unpack method for COOLCONF: overrides the base Unpack +func (c *COOLCONF_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + c.Semin = uint8((registerValue >> 0) & 0xF) // Extract 4 bits for Semin + c.Seup = uint8((registerValue >> 5) & 0x3) // Extract 2 bits for Seup + c.Semax = uint8((registerValue >> 8) & 0xF) // Extract 4 bits for Semax + c.Sedn = uint8((registerValue >> 13) & 0x3) // Extract 2 bits for Sedn + c.Seimin = (registerValue & (1 << 15)) != 0 // Extract 1 bit for Seimin + c.Sgt = uint8((registerValue >> 16) & 0x7F) // Extract 7 bits for Sgt + c.Sfilt = (registerValue & (1 << 24)) != 0 // Extract 1 bit for Sfilt +} + +// DCCTRL_Register struct to represent the DCCTRL register +type DCCTRL_Register struct { + Register + DcTime uint16 // Upper PWM on time limit for commutation (10 bits) + DcSg uint8 // Max. PWM on time for step loss detection using dcStep (8 bits) +} + +// NewDCCTRL creates a new DCCTRL register instance +func NewDCCTRL() *DCCTRL_Register { + return &DCCTRL_Register{ + Register: Register{ + RegisterAddr: DCCTRL, + }, + } +} + +// Pack method for DCCTRL: overrides the base Pack +func (d *DCCTRL_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(d.DcTime&0x3FF) << 0 // DcTime: 10 bits + registerValue |= uint32(d.DcSg&0xFF) << 16 // DcSg: 8 bits + + return registerValue +} + +// Unpack method for DCCTRL: overrides the base Unpack +func (d *DCCTRL_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + d.DcTime = uint16((registerValue >> 0) & 0x3FF) // Extract 10 bits for DcTime + d.DcSg = uint8((registerValue >> 16) & 0xFF) // Extract 8 bits for DcSg +} + +// DRV_STATUS_Register struct to represent the DRV_STATUS register +type DRV_STATUS_Register struct { + Register + SgResult uint16 // stallGuard2 result or motor temperature estimation in standstill (9 bits) + S2vsa bool // Short to supply indicator phase A + S2vsb bool // Short to supply indicator phase B + Stealth bool // stealthChop indicator + FsActive bool // Full step active indicator + CsActual uint8 // Actual motor current / smart energy current (5 bits) + StallGuard bool // stallGuard2 status + Ot bool // Overtemperature flag + Otpw bool // Overtemperature pre-warning flag + S2ga bool // Short to ground indicator phase A + S2gb bool // Short to ground indicator phase B + Ola bool // Open load indicator phase A + Olb bool // Open load indicator phase B + Stst bool // Standstill indicator +} + +// NewDRV_STATUS creates a new DRV_STATUS register instance +func NewDRV_STATUS() *DRV_STATUS_Register { + return &DRV_STATUS_Register{ + Register: Register{ + RegisterAddr: DRV_STATUS, + }, + } +} + +// Pack method for DRV_STATUS: overrides the base Pack +func (d *DRV_STATUS_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(d.SgResult&0x1FF) << 0 // SgResult: 9 bits + if d.S2vsa { + registerValue |= 1 << 12 // S2vsa: 1 bit + } + if d.S2vsb { + registerValue |= 1 << 13 // S2vsb: 1 bit + } + if d.Stealth { + registerValue |= 1 << 14 // Stealth: 1 bit + } + if d.FsActive { + registerValue |= 1 << 15 // FsActive: 1 bit + } + registerValue |= uint32(d.CsActual&0x1F) << 16 // CsActual: 5 bits + if d.StallGuard { + registerValue |= 1 << 24 // StallGuard: 1 bit + } + if d.Ot { + registerValue |= 1 << 25 // Ot: 1 bit + } + if d.Otpw { + registerValue |= 1 << 26 // Otpw: 1 bit + } + if d.S2ga { + registerValue |= 1 << 27 // S2ga: 1 bit + } + if d.S2gb { + registerValue |= 1 << 28 // S2gb: 1 bit + } + if d.Ola { + registerValue |= 1 << 29 // Ola: 1 bit + } + if d.Olb { + registerValue |= 1 << 30 // Olb: 1 bit + } + if d.Stst { + registerValue |= 1 << 31 // Stst: 1 bit + } + + return registerValue +} + +// Unpack method for DRV_STATUS: overrides the base Unpack +func (d *DRV_STATUS_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + d.SgResult = uint16((registerValue >> 0) & 0x1FF) // Extract 9 bits for SgResult + d.S2vsa = (registerValue & (1 << 12)) != 0 // Extract 1 bit for S2vsa + d.S2vsb = (registerValue & (1 << 13)) != 0 // Extract 1 bit for S2vsb + d.Stealth = (registerValue & (1 << 14)) != 0 // Extract 1 bit for Stealth + d.FsActive = (registerValue & (1 << 15)) != 0 // Extract 1 bit for FsActive + d.CsActual = uint8((registerValue >> 16) & 0x1F) // Extract 5 bits for CsActual + d.StallGuard = (registerValue & (1 << 24)) != 0 // Extract 1 bit for StallGuard + d.Ot = (registerValue & (1 << 25)) != 0 // Extract 1 bit for Ot + d.Otpw = (registerValue & (1 << 26)) != 0 // Extract 1 bit for Otpw + d.S2ga = (registerValue & (1 << 27)) != 0 // Extract 1 bit for S2ga + d.S2gb = (registerValue & (1 << 28)) != 0 // Extract 1 bit for S2gb + d.Ola = (registerValue & (1 << 29)) != 0 // Extract 1 bit for Ola + d.Olb = (registerValue & (1 << 30)) != 0 // Extract 1 bit for Olb + d.Stst = (registerValue & (1 << 31)) != 0 // Extract 1 bit for Stst +} + +// PWMCONF_Register struct to represent the PWMCONF register +type PWMCONF_Register struct { + Register + PwmOfs uint8 // User defined PWM amplitude offset (8 bits) + PwmGrad uint8 // User defined PWM amplitude gradient (8 bits) + PwmFreq uint8 // PWM frequency selection (2 bits) + PwmAutoscale bool // Enable PWM automatic amplitude scaling (1 bit) + PwmAutograd bool // PWM automatic gradient adaptation (1 bit) + Freewheel uint8 // Standstill option when motor current setting is zero (2 bits) + PwmReg uint8 // Regulation loop gradient (4 bits) + PwmLim uint8 // PWM automatic scale amplitude limit when switching on (4 bits) +} + +// NewPWMCONF creates a new PWMCONF register instance +func NewPWMCONF() *PWMCONF_Register { + return &PWMCONF_Register{ + Register: Register{ + RegisterAddr: PWMCONF, + }, + } +} + +// Pack method for PWMCONF: overrides the base Pack +func (p *PWMCONF_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(p.PwmOfs&0xFF) << 0 // PwmOfs: 8 bits + registerValue |= uint32(p.PwmGrad&0xFF) << 8 // PwmGrad: 8 bits + registerValue |= uint32(p.PwmFreq&0x3) << 16 // PwmFreq: 2 bits + if p.PwmAutoscale { + registerValue |= 1 << 18 // PwmAutoscale: 1 bit + } + if p.PwmAutograd { + registerValue |= 1 << 19 // PwmAutograd: 1 bit + } + registerValue |= uint32(p.Freewheel&0x3) << 20 // Freewheel: 2 bits + registerValue |= uint32(p.PwmReg&0xF) << 24 // PwmReg: 4 bits + registerValue |= uint32(p.PwmLim&0xF) << 28 // PwmLim: 4 bits + + return registerValue +} + +// Unpack method for PWMCONF: overrides the base Unpack +func (p *PWMCONF_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + p.PwmOfs = uint8((registerValue >> 0) & 0xFF) // Extract 8 bits for PwmOfs + p.PwmGrad = uint8((registerValue >> 8) & 0xFF) // Extract 8 bits for PwmGrad + p.PwmFreq = uint8((registerValue >> 16) & 0x3) // Extract 2 bits for PwmFreq + p.PwmAutoscale = (registerValue & (1 << 18)) != 0 // Extract 1 bit for PwmAutoscale + p.PwmAutograd = (registerValue & (1 << 19)) != 0 // Extract 1 bit for PwmAutograd + p.Freewheel = uint8((registerValue >> 20) & 0x3) // Extract 2 bits for Freewheel + p.PwmReg = uint8((registerValue >> 24) & 0xF) // Extract 4 bits for PwmReg + p.PwmLim = uint8((registerValue >> 28) & 0xF) // Extract 4 bits for PwmLim +} + +// PWM_SCALE_Register struct to represent the PWM_SCALE register +type PWM_SCALE_Register struct { + Register + PwmScaleSum uint8 // Actual PWM duty cycle (8 bits) + PwmScaleAuto uint16 // Result of the automatic amplitude regulation based on current measurement (9 bits) +} + +// NewPWM_SCALE creates a new PWM_SCALE register instance +func NewPWM_SCALE() *PWM_SCALE_Register { + return &PWM_SCALE_Register{ + Register: Register{ + RegisterAddr: PWM_SCALE, + }, + } +} + +// Pack method for PWM_SCALE: overrides the base Pack +func (p *PWM_SCALE_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(p.PwmScaleSum&0xFF) << 0 // PwmScaleSum: 8 bits + registerValue |= uint32(p.PwmScaleAuto&0x1FF) << 16 // PwmScaleAuto: 9 bits + + return registerValue +} + +// Unpack method for PWM_SCALE: overrides the base Unpack +func (p *PWM_SCALE_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + p.PwmScaleSum = uint8((registerValue >> 0) & 0xFF) // Extract 8 bits for PwmScaleSum + p.PwmScaleAuto = uint16((registerValue >> 16) & 0x1FF) // Extract 9 bits for PwmScaleAuto +} + +// PWM_AUTO_Register struct to represent the PWM_AUTO register +type PWM_AUTO_Register struct { + Register + PwmOfsAuto uint8 // Automatically determined offset value (8 bits) + PwmGradAuto uint8 // Automatically determined gradient value (8 bits) +} + +// NewPWM_AUTO creates a new PWM_AUTO register instance +func NewPWM_AUTO() *PWM_AUTO_Register { + return &PWM_AUTO_Register{ + Register: Register{ + RegisterAddr: PWM_AUTO, + }, + } +} + +// Pack method for PWM_AUTO: overrides the base Pack +func (p *PWM_AUTO_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(p.PwmOfsAuto&0xFF) << 0 // PwmOfsAuto: 8 bits + registerValue |= uint32(p.PwmGradAuto&0xFF) << 16 // PwmGradAuto: 8 bits + + return registerValue +} + +// Unpack method for PWM_AUTO: overrides the base Unpack +func (p *PWM_AUTO_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + p.PwmOfsAuto = uint8((registerValue >> 0) & 0xFF) // Extract 8 bits for PwmOfsAuto + p.PwmGradAuto = uint8((registerValue >> 16) & 0xFF) // Extract 8 bits for PwmGradAuto +} + +// MSCNT_Register struct to represent the MSCNT register (10-bit value) +type MSCNT_Register struct { + Register + Value uint16 // Microstep counter value (10 bits) +} + +// NewMSCNT creates a new MSCNT register instance +func NewMSCNT() *MSCNT_Register { + return &MSCNT_Register{ + Register: Register{ + RegisterAddr: MSCNT, + }, + } +} + +// Pack method for MSCNT: combines the 10-bit value into a 16-bit value +func (m *MSCNT_Register) Pack() uint16 { + return m.Value & 0x3FF // Mask the value to ensure it is within the 10-bit range (0-1023) +} + +// Unpack method for MSCNT: extracts the 10-bit value from a 16-bit value +func (m *MSCNT_Register) Unpack(registerValue uint16) { + m.Value = registerValue & 0x3FF // Mask to extract the 10-bit value (0-1023) +} + +// VDCMIN_Register struct for VDCMIN register (23 bits) +type VDCMIN_Register struct { + Register + Value uint32 // 23-bit value +} + +// NewVDCMIN creates a new VDCMIN register instance +func NewVDCMIN() *VDCMIN_Register { + return &VDCMIN_Register{ + Register: Register{ + RegisterAddr: VDCMIN, + }, + } +} + +// Pack method for VDCMIN: packs the 23-bit value into a 32-bit value +func (v *VDCMIN_Register) Pack() uint32 { + return v.Value & 0x7FFFFF // Mask to 23 bits +} + +// Unpack method for VDCMIN: unpacks the 23-bit value from a 32-bit value +func (v *VDCMIN_Register) Unpack(registerValue uint32) { + v.Value = registerValue & 0x7FFFFF // Mask to 23 bits +} + +// XLATCH_Register struct for XLATCH register (32 bits) +type XLATCH_Register struct { + Register + Value uint32 // 32-bit value +} + +// NewXLATCH creates a new XLATCH register instance +func NewXLATCH() *XLATCH_Register { + return &XLATCH_Register{ + Register: Register{ + RegisterAddr: XLATCH, + }, + } +} + +// Pack method for XLATCH: directly returns the 32-bit value +func (x *XLATCH_Register) Pack() uint32 { + return x.Value // No mask needed, since it's 32 bits +} + +// Unpack method for XLATCH: unpacks the 32-bit value from a 32-bit register +func (x *XLATCH_Register) Unpack(registerValue uint32) { + x.Value = registerValue // Direct assignment since it's 32 bits +} + +// RAMPMODE_Register struct for RAMPMODE register (2 bits) +type RAMPMODE_Register struct { + Register + mode RampMode // Mode is now an enum-like type + comm RegisterComm + driverIndex uint8 +} +type RampMode uint8 + +const ( + PositioningMode RampMode = iota // 0 + VelocityPositiveMode // 1 + VelocityNegativeMode // 2 + HoldMode // 3 +) + +func NewRAMPMODE(comm RegisterComm, driverIndex uint8) *RAMPMODE_Register { + return &RAMPMODE_Register{ + Register: Register{ + RegisterAddr: RAMPMODE, + }, + driverIndex: driverIndex, + comm: comm, + mode: PositioningMode, // Default to Positioning Mode + } +} + +// SetMode sets the mode of the RAMPMODE register +func (r *RAMPMODE_Register) SetMode(mode RampMode) error { + r.mode = mode + registerValue := r.Pack() + return r.comm.WriteRegister(r.RegisterAddr, uint32(registerValue), r.driverIndex) +} + +// GetMode returns the current mode of the RAMPMODE register +func (r *RAMPMODE_Register) GetMode() (RampMode, error) { + registerValue, err := r.comm.ReadRegister(r.RegisterAddr, r.driverIndex) + if err != nil { + return 0, err //Defaults to Postioning Mode + } + + // Unpack the register value to get the mode + r.Unpack(uint8(registerValue)) + return r.mode, nil + +} + +// Pack method for RAMPMODE: packs the mode value into a single byte (now using enums) +func (r *RAMPMODE_Register) Pack() uint8 { + return uint8(r.mode) // Simply cast the mode to uint8 +} + +// Unpack method for RAMPMODE: unpacks the mode value from a byte +func (r *RAMPMODE_Register) Unpack(registerValue uint8) { + r.mode = RampMode(registerValue & 0x03) // Mask to 2 bits +} + +// String method to display the mode as a string (useful for logging or debugging) +func (r RampMode) String() string { + switch r { + case PositioningMode: + return "Positioning Mode" + case VelocityPositiveMode: + return "Velocity Mode (Positive VMAX)" + case VelocityNegativeMode: + return "Velocity Mode (Negative VMAX)" + case HoldMode: + return "Hold Mode" + default: + return "Unknown Mode" + } +} + +// XACTUAL_Register struct for XACTUAL register (32 bits) +type XACTUAL_Register struct { + Register + Value uint32 // 32-bit value +} + +// NewXACTUAL creates a new XACTUAL register instance +func NewXACTUAL() *XACTUAL_Register { + return &XACTUAL_Register{ + Register: Register{ + RegisterAddr: XACTUAL, + }, + } +} + +// Pack method for XACTUAL: returns the 32-bit value +func (x *XACTUAL_Register) Pack() uint32 { + return x.Value // 32 bits, no masking needed +} + +// Unpack method for XACTUAL: unpacks the 32-bit value +func (x *XACTUAL_Register) Unpack(registerValue uint32) { + x.Value = registerValue // Direct assignment since it's 32 bits +} + +// VACTUAL_Register struct for VACTUAL register (24 bits) +type VACTUAL_Register struct { + Register + Value uint32 // 24-bit value (stored in a 32-bit field) +} + +// NewVACTUAL creates a new VACTUAL register instance +func NewVACTUAL() *VACTUAL_Register { + return &VACTUAL_Register{ + Register: Register{ + RegisterAddr: VACTUAL, + }, + } +} + +// Pack method for VACTUAL: packs the 24-bit value into a 32-bit value +func (v *VACTUAL_Register) Pack() uint32 { + return v.Value & 0xFFFFFF // Mask to 24 bits +} + +// Unpack method for VACTUAL: unpacks the 24-bit value from a 32-bit value +func (v *VACTUAL_Register) Unpack(registerValue uint32) { + v.Value = registerValue & 0xFFFFFF // Mask to 24 bits +} + +// VSTART_Register struct for VSTART register (18 bits) +type VSTART_Register struct { + Register + Value uint32 // 18-bit value +} + +// NewVSTART creates a new VSTART register instance +func NewVSTART() *VSTART_Register { + return &VSTART_Register{ + Register: Register{ + RegisterAddr: VSTART, + }, + } +} + +// Pack method for VSTART: packs the 18-bit value into a 16-bit value +func (v *VSTART_Register) Pack() uint32 { + return v.Value & 0x3FFFF // Mask to 18 bits +} + +// Unpack method for VSTART: unpacks the 18-bit value from a 16-bit value +func (v *VSTART_Register) Unpack(registerValue uint32) { + v.Value = registerValue & 0x3FFFF // Mask to 18 bits +} + +// A1_Register struct for A1 register (16 bits) +type A1_Register struct { + Register + Value uint16 // 16-bit value +} + +// NewA1 creates a new A1 register instance +func NewA1() *A1_Register { + return &A1_Register{ + Register: Register{ + RegisterAddr: A_1, + }, + } +} + +// Pack method for A1: returns the 16-bit value +func (a *A1_Register) Pack() uint16 { + return a.Value // 16 bits, no masking needed +} + +// Unpack method for A1: unpacks the 16-bit value +func (a *A1_Register) Unpack(registerValue uint16) { + a.Value = registerValue // Direct assignment since it's 16 bits +} + +// V1_Register struct for V1 register (20 bits) +type V1_Register struct { + Register + Value uint32 // 20-bit value (stored in a 32-bit field) +} + +// NewV1 creates a new V1 register instance +func NewV1() *V1_Register { + return &V1_Register{ + Register: Register{ + RegisterAddr: V_1, + }, + } +} + +// Pack method for V1: packs the 20-bit value into a 32-bit value +func (v *V1_Register) Pack() uint32 { + return v.Value & 0xFFFFF // Mask to 20 bits +} + +// Unpack method for V1: unpacks the 20-bit value from a 32-bit value +func (v *V1_Register) Unpack(registerValue uint32) { + v.Value = registerValue & 0xFFFFF // Mask to 20 bits +} + +// AMAX_Register struct for AMAX register (16 bits) +type AMAX_Register struct { + Register + Value uint16 // 16-bit value +} + +// NewAMAX creates a new AMAX register instance +func NewAMAX() *AMAX_Register { + return &AMAX_Register{ + Register: Register{ + RegisterAddr: AMAX, + }, + } +} + +// Pack method for AMAX: returns the 16-bit value +func (a *AMAX_Register) Pack() uint16 { + return a.Value // 16 bits, no masking needed +} + +// Unpack method for AMAX: unpacks the 16-bit value +func (a *AMAX_Register) Unpack(registerValue uint16) { + a.Value = registerValue // Direct assignment since it's 16 bits +} + +// VMAX_Register struct for VMAX register (23 bits) +type VMAX_Register struct { + Register + Value uint32 // 23-bit value (stored in a 32-bit field) +} + +// NewVMAX creates a new VMAX register instance +func NewVMAX() *VMAX_Register { + return &VMAX_Register{ + Register: Register{ + RegisterAddr: VMAX, + }, + } +} + +// Pack method for VMAX: packs the 23-bit value into a 32-bit value +func (v *VMAX_Register) Pack() uint32 { + return v.Value & 0x7FFFFF // Mask to 23 bits +} + +// Unpack method for VMAX: unpacks the 23-bit value from a 32-bit value +func (v *VMAX_Register) Unpack(registerValue uint32) { + v.Value = registerValue & 0x7FFFFF // Mask to 23 bits +} + +// D1_Register struct for D1 register (16 bits) +type D1_Register struct { + Register + Value uint16 // 16-bit value +} + +// NewD1 creates a new D1 register instance +func NewD1() *D1_Register { + return &D1_Register{ + Register: Register{ + RegisterAddr: D_1, + }, + } +} + +// Pack method for D1: returns the 16-bit value +func (d *D1_Register) Pack() uint16 { + return d.Value // 16 bits, no masking needed +} + +// Unpack method for D1: unpacks the 16-bit value +func (d *D1_Register) Unpack(registerValue uint16) { + d.Value = registerValue // Direct assignment since it's 16 bits +} + +// VSTOP_Register struct for VSTOP register (18 bits) +type VSTOP_Register struct { + Register + Value uint32 // 18-bit value (stored in a 32-bit field) +} + +// NewVSTOP creates a new VSTOP register instance +func NewVSTOP() *VSTOP_Register { + return &VSTOP_Register{ + Register: Register{ + RegisterAddr: VSTOP, + }, + } +} + +// Pack method for VSTOP: packs the 18-bit value into a 32-bit value +func (v *VSTOP_Register) Pack() uint32 { + return v.Value & 0x3FFFF // Mask to 18 bits +} + +// Unpack method for VSTOP: unpacks the 18-bit value from a 32-bit value +func (v *VSTOP_Register) Unpack(registerValue uint32) { + v.Value = registerValue & 0x3FFFF // Mask to 18 bits +} + +// TZEROWAIT_Register struct for TZEROWAIT register (16 bits) +type TZEROWAIT_Register struct { + Register + Value uint16 // 16-bit value +} + +// NewTZEROWAIT creates a new TZEROWAIT register instance +func NewTZEROWAIT() *TZEROWAIT_Register { + return &TZEROWAIT_Register{ + Register: Register{ + RegisterAddr: TZEROWAIT, + }, + } +} + +// Pack method for TZEROWAIT: returns the 16-bit value +func (t *TZEROWAIT_Register) Pack() uint16 { + return t.Value // 16 bits, no masking needed +} + +// Unpack method for TZEROWAIT: unpacks the 16-bit value +func (t *TZEROWAIT_Register) Unpack(registerValue uint16) { + t.Value = registerValue // Direct assignment since it's 16 bits +} + +// XTARGET_Register struct for XTARGET register (32 bits) +type XTARGET_Register struct { + Register + Value uint32 // 32-bit value +} + +// NewXTARGET creates a new XTARGET register instance +func NewXTARGET() *XTARGET_Register { + return &XTARGET_Register{ + Register: Register{ + RegisterAddr: XTARGET, + }, + } +} + +// Pack method for XTARGET: returns the 32-bit value +func (x *XTARGET_Register) Pack() uint32 { + return x.Value // 32 bits, no masking needed +} + +// Unpack method for XTARGET: unpacks the 32-bit value +func (x *XTARGET_Register) Unpack(registerValue uint32) { + x.Value = registerValue // Direct assignment since it's 32 bits +} + +// X_COMPARE_Register struct for X_COMPARE register (32 bits) +type X_COMPARE_Register struct { + Register + Value uint32 // 32-bit value for position comparison +} + +// NewX_COMPARE creates a new X_COMPARE register instance +func NewX_COMPARE() *X_COMPARE_Register { + return &X_COMPARE_Register{ + Register: Register{ + RegisterAddr: X_COMPARE, + }, + } +} + +// Pack method for X_COMPARE: returns the 32-bit value +func (x *X_COMPARE_Register) Pack() uint32 { + return x.Value // 32 bits, no masking needed +} + +// Unpack method for X_COMPARE: unpacks the 32-bit value +func (x *X_COMPARE_Register) Unpack(registerValue uint32) { + x.Value = registerValue // Direct assignment since it's 32 bits +} + +// GLOBAL_SCALER_Register struct for GLOBAL SCALER register (8 bits) +type GLOBAL_SCALER_Register struct { + Register + Value uint8 // 8-bit value for global motor current scaling +} + +// NewGLOBAL_SCALER creates a new GLOBAL_SCALER register instance +func NewGLOBAL_SCALER() *GLOBAL_SCALER_Register { + return &GLOBAL_SCALER_Register{ + Register: Register{ + RegisterAddr: GLOBAL_SCALER, + }, + } +} + +// Pack method for GLOBAL_SCALER: returns the 8-bit value +func (g *GLOBAL_SCALER_Register) Pack() uint8 { + return g.Value // 8 bits, no masking needed +} + +// Unpack method for GLOBAL_SCALER: unpacks the 8-bit value +func (g *GLOBAL_SCALER_Register) Unpack(registerValue uint8) { + g.Value = registerValue // Direct assignment since it's 8 bits +} + +// TPOWERDOWN_Register struct for TPOWERDOWN register (8 bits) +type TPOWERDOWN_Register struct { + Register + Value uint8 // 8-bit value for time delay after standstill +} + +// NewTPOWERDOWN creates a new TPOWERDOWN register instance +func NewTPOWERDOWN() *TPOWERDOWN_Register { + return &TPOWERDOWN_Register{ + Register: Register{ + RegisterAddr: TPOWERDOWN, + }, + } +} + +// Pack method for TPOWERDOWN: returns the 8-bit value +func (t *TPOWERDOWN_Register) Pack() uint8 { + return t.Value // 8 bits, no masking needed +} + +// Unpack method for TPOWERDOWN: unpacks the 8-bit value +func (t *TPOWERDOWN_Register) Unpack(registerValue uint8) { + t.Value = registerValue // Direct assignment since it's 8 bits +} + +// PWMTHRS_Register struct for PWMTHRS register (20 bits) +type PWMTHRS_Register struct { + Register + Value uint32 // 20-bit value (stored in a 32-bit field) +} + +// NewPWMTHRS creates a new PWMTHRS register instance +func NewPWMTHRS() *PWMTHRS_Register { + return &PWMTHRS_Register{ + Register: Register{ + RegisterAddr: TPWMTHRS, + }, + } +} + +// Pack method for PWMTHRS: packs the 20-bit value into a 32-bit value +func (p *PWMTHRS_Register) Pack() uint32 { + return p.Value & 0xFFFFF // Mask to 20 bits +} + +// Unpack method for PWMTHRS: unpacks the 20-bit value from a 32-bit value +func (p *PWMTHRS_Register) Unpack(registerValue uint32) { + p.Value = registerValue & 0xFFFFF // Mask to 20 bits +} + +// TCOOLTHRS_Register struct for TCOOLTHRS register (20 bits) +type TCOOLTHRS_Register struct { + Register + Value uint32 // 20-bit value (stored in a 32-bit field) +} + +// NewTCOOLTHRS creates a new TCOOLTHRS register instance +func NewTCOOLTHRS() *TCOOLTHRS_Register { + return &TCOOLTHRS_Register{ + Register: Register{ + RegisterAddr: TCOOLTHRS, + }, + } +} + +// Pack method for TCOOLTHRS: packs the 20-bit value into a 32-bit value +func (t *TCOOLTHRS_Register) Pack() uint32 { + return t.Value & 0xFFFFF // Mask to 20 bits +} + +// Unpack method for TCOOLTHRS: unpacks the 20-bit value from a 32-bit value +func (t *TCOOLTHRS_Register) Unpack(registerValue uint32) { + t.Value = registerValue & 0xFFFFF // Mask to 20 bits +} + +// THIGH_Register struct for THIGH register (16 bits) +type THIGH_Register struct { + Register + Value uint16 // 16-bit value +} + +// NewTHIGH creates a new THIGH register instance +func NewTHIGH() *THIGH_Register { + return &THIGH_Register{ + Register: Register{ + RegisterAddr: THIGH, + }, + } +} + +// Pack method for THIGH: returns the 16-bit value +func (t *THIGH_Register) Pack() uint16 { + return t.Value // 16 bits, no masking needed +} + +// Unpack method for THIGH: unpacks the 16-bit value +func (t *THIGH_Register) Unpack(registerValue uint16) { + t.Value = registerValue // Direct assignment since it's 16 bits +} + +// DMAX_Register struct for DMAX register (16 bits) +type DMAX_Register struct { + Register + Value uint16 // 16-bit value for deceleration between VMAX and VSTOP +} + +// NewDMAX creates a new DMAX register instance +func NewDMAX() *DMAX_Register { + return &DMAX_Register{ + Register: Register{ + RegisterAddr: DMAX, + }, + } +} + +// Pack method for DMAX: returns the 16-bit value +func (d *DMAX_Register) Pack() uint16 { + return d.Value // 16 bits, no masking needed +} + +// Unpack method for DMAX: unpacks the 16-bit value +func (d *DMAX_Register) Unpack(registerValue uint16) { + d.Value = registerValue // Direct assignment since it's 16 bits +} + +// TSTEP_Register struct for TSTEP register (20 bits) +type TSTEP_Register struct { + Register + Value uint32 // 20-bit value (stored in a 32-bit field) +} + +// NewTSTEP creates a new TSTEP register instance +func NewTSTEP() *TSTEP_Register { + return &TSTEP_Register{ + Register: Register{ + RegisterAddr: TSTEP, + }, + } +} + +// Pack method for TSTEP: packs the 20-bit value into a 32-bit value +func (t *TSTEP_Register) Pack() uint32 { + return t.Value & 0xFFFFF // Mask to 20 bits +} + +// Unpack method for TSTEP: unpacks the 20-bit value from a 32-bit value +func (t *TSTEP_Register) Unpack(registerValue uint32) { + t.Value = registerValue & 0xFFFFF // Mask to 20 bits +} + +// X_ENC_Register struct for X_ENC register (32 bits) +type X_ENC_Register struct { + Register + Value int32 // 32-bit signed value for actual encoder position +} + +// NewX_ENC creates a new X_ENC register instance +func NewX_ENC() *X_ENC_Register { + return &X_ENC_Register{ + Register: Register{ + RegisterAddr: X_ENC, + }, + } +} + +// Pack method for X_ENC: returns the 32-bit signed value +func (x *X_ENC_Register) Pack() int32 { + return x.Value // 32 bits, no masking needed for signed integer +} + +// Unpack method for X_ENC: unpacks the 32-bit signed value +func (x *X_ENC_Register) Unpack(registerValue int32) { + x.Value = registerValue // Direct assignment since it's 32 bits signed integer +} + +// ENC_CONST_Register struct for ENC_CONST register (32 bits) +type ENC_CONST_Register struct { + Register + Value int32 // 32-bit signed accumulation constant +} + +// NewENC_CONST creates a new ENC_CONST register instance +func NewENC_CONST() *ENC_CONST_Register { + return &ENC_CONST_Register{ + Register: Register{ + RegisterAddr: ENC_CONST, + }, + } +} + +// Pack method for ENC_CONST: returns the 32-bit signed accumulation constant +func (e *ENC_CONST_Register) Pack() int32 { + return e.Value // 32 bits, no masking needed for signed integer +} + +// Unpack method for ENC_CONST: unpacks the 32-bit signed accumulation constant +func (e *ENC_CONST_Register) Unpack(registerValue int32) { + e.Value = registerValue // Direct assignment since it's 32 bits signed integer +} + +// ENC_LATCH_Register struct for ENC_LATCH register (32 bits) +type ENC_LATCH_Register struct { + Register + Value int32 // 32-bit signed value for encoder position latched on N event +} + +// NewENC_LATCH creates a new ENC_LATCH register instance +func NewENC_LATCH() *ENC_LATCH_Register { + return &ENC_LATCH_Register{ + Register: Register{ + RegisterAddr: ENC_LATCH, + }, + } +} + +// Pack method for ENC_LATCH: returns the 32-bit signed value +func (e *ENC_LATCH_Register) Pack() int32 { + return e.Value // 32 bits, no masking needed for signed integer +} + +// Unpack method for ENC_LATCH: unpacks the 32-bit signed value +func (e *ENC_LATCH_Register) Unpack(registerValue int32) { + e.Value = registerValue // Direct assignment since it's 32 bits signed integer +} + +// ENC_DEVIATION_Register struct for ENC_DEVIATION register (20 bits) +type ENC_DEVIATION_Register struct { + Register + Value uint32 // 20-bit unsigned value for maximum deviation +} + +// NewENC_DEVIATION creates a new ENC_DEVIATION register instance +func NewENC_DEVIATION() *ENC_DEVIATION_Register { + return &ENC_DEVIATION_Register{ + Register: Register{ + RegisterAddr: ENC_DEVIATION, + }, + } +} + +// Pack method for ENC_DEVIATION: packs the 20-bit value into a 32-bit value +func (e *ENC_DEVIATION_Register) Pack() uint32 { + return e.Value & 0xFFFFF // Mask to 20 bits +} + +// Unpack method for ENC_DEVIATION: unpacks the 20-bit value from a 32-bit value +func (e *ENC_DEVIATION_Register) Unpack(registerValue uint32) { + e.Value = registerValue & 0xFFFFF // Mask to 20 bits +} + +// MSCURACT_Register struct for MSCURACT register (18 bits) +type MSCURACT_Register struct { + Register + CUR_B int16 // 9-bit signed value for motor phase B (sine wave) + CUR_A int16 // 9-bit signed value for motor phase A (cosine wave) +} + +// NewMSCURACT creates a new MSCURACT register instance +func NewMSCURACT() *MSCURACT_Register { + return &MSCURACT_Register{ + Register: Register{ + RegisterAddr: MSCURACT, + }, + } +} + +// Pack method for MSCURACT: packs the 9-bit signed values for CUR_B and CUR_A into a 32-bit value +func (m *MSCURACT_Register) Pack() uint32 { + return uint32(m.CUR_A<<16 | m.CUR_B) // Combine CUR_A and CUR_B into a 32-bit value +} + +// Unpack method for MSCURACT: unpacks the 32-bit value into CUR_B and CUR_A +func (m *MSCURACT_Register) Unpack(registerValue uint32) { + m.CUR_B = int16(registerValue & 0x1FF) // Mask to get the lower 9 bits for CUR_B + m.CUR_A = int16((registerValue >> 16) & 0x1FF) // Mask to get the next 9 bits for CUR_A +} + +// LOST_STEPS_Register struct for LOST_STEPS register (20 bits) +type LOST_STEPS_Register struct { + Register + Value uint32 // 20-bit unsigned value for lost steps count +} + +// NewLOST_STEPS creates a new LOST_STEPS register instance +func NewLOST_STEPS() *LOST_STEPS_Register { + return &LOST_STEPS_Register{ + Register: Register{ + RegisterAddr: LOST_STEPS, + }, + } +} + +// Pack method for LOST_STEPS: returns the 20-bit value +func (l *LOST_STEPS_Register) Pack() uint32 { + return l.Value & 0xFFFFF // Mask to 20 bits +} + +// Unpack method for LOST_STEPS: unpacks the 20-bit value from a 32-bit value +func (l *LOST_STEPS_Register) Unpack(registerValue uint32) { + l.Value = registerValue & 0xFFFFF // Mask to 20 bits +} + +// MSLUTSEL_Register struct for MSLUTSEL register (32 bits) +type MSLUTSEL_Register struct { + Register + X3 uint8 // 3-bit value for LUT segment 3 start + X2 uint8 // 3-bit value for LUT segment 2 start + X1 uint8 // 3-bit value for LUT segment 1 start + W3 uint8 // 2-bit value for LUT width control W3 + W2 uint8 // 2-bit value for LUT width control W2 + W1 uint8 // 2-bit value for LUT width control W1 + W0 uint8 // 2-bit value for LUT width control W0 +} + +// NewMSLUTSEL creates a new MSLUTSEL register instance +func NewMSLUTSEL() *MSLUTSEL_Register { + return &MSLUTSEL_Register{ + Register: Register{ + RegisterAddr: MSLUTSEL, + }, + } +} + +// Pack method for MSLUTSEL: combines all the fields into a 32-bit value +func (m *MSLUTSEL_Register) Pack() uint32 { + return uint32(m.X3<<27 | m.X2<<24 | m.X1<<21 | m.W3<<18 | m.W2<<16 | m.W1<<14 | m.W0<<12) // Combine fields into a 32-bit value +} + +// Unpack method for MSLUTSEL: unpacks the 32-bit value into individual fields +func (m *MSLUTSEL_Register) Unpack(registerValue uint32) { + m.X3 = uint8((registerValue >> 27) & 0x07) // Extract the 3 bits for X3 + m.X2 = uint8((registerValue >> 24) & 0x07) // Extract the 3 bits for X2 + m.X1 = uint8((registerValue >> 21) & 0x07) // Extract the 3 bits for X1 + m.W3 = uint8((registerValue >> 18) & 0x03) // Extract the 2 bits for W3 + m.W2 = uint8((registerValue >> 16) & 0x03) // Extract the 2 bits for W2 + m.W1 = uint8((registerValue >> 14) & 0x03) // Extract the 2 bits for W1 + m.W0 = uint8((registerValue >> 12) & 0x03) // Extract the 2 bits for W0 +} + +// MSLUT_Register struct for MSLUT register (32 bits) +type MSLUT_Register struct { + Register + Value uint32 // 32-bit value for microstep table entry +} + +// NewMSLUT creates a new MSLUT register instance +func NewMSLUT() *MSLUT_Register { + return &MSLUT_Register{ + Register: Register{ + RegisterAddr: MSLUT0, + }, + } +} + +// Pack method for MSLUT: returns the 32-bit value for the microstep entry +func (m *MSLUT_Register) Pack() uint32 { + return m.Value // 32 bits, no masking needed +} + +// Unpack method for MSLUT: unpacks the 32-bit value into the microstep entry +func (m *MSLUT_Register) Unpack(registerValue uint32) { + m.Value = registerValue // Direct assignment since it's 32 bits +} + +// MSLUTSTART_Register struct for MSLUTSTART register (16 bits) +type MSLUTSTART_Register struct { + Register + START_SIN int8 // 8-bit signed value for the absolute current at microstep entry 0 + START_SIN90 int8 // 8-bit signed value for the absolute current at microstep entry 256 +} + +// NewMSLUTSTART creates a new MSLUTSTART register instance +func NewMSLUTSTART() *MSLUTSTART_Register { + return &MSLUTSTART_Register{ + Register: Register{ + RegisterAddr: MSLUTSTART, + }, + } +} + +// Pack method for MSLUTSTART: combines START_SIN and START_SIN90 into a 16-bit value +func (m *MSLUTSTART_Register) Pack() uint16 { + return uint16(m.START_SIN) | (uint16(m.START_SIN90) << 8) // Combine the 8-bit values into a 16-bit value +} + +// Unpack method for MSLUTSTART: unpacks the 16-bit value into START_SIN and START_SIN90 +func (m *MSLUTSTART_Register) Unpack(registerValue uint16) { + m.START_SIN = int8(registerValue & 0xFF) // Extract the lower 8 bits for START_SIN + m.START_SIN90 = int8((registerValue >> 8) & 0xFF) // Extract the upper 8 bits for START_SIN90 +} + +// Function to calculate the sine wave values for the microstep table +func calculateSineWaveTable() []int { + // Create a slice to store the sine wave table + table := make([]int, 256) + + // Loop through each table index (i) + for i := 0; i < 256; i++ { + // Calculate the sine value and scale it by 248 + sineValue := 248 * math.Sin(2*math.Pi*float32(i)/1024) + + // Round the result and subtract 1 + roundedValue := int(math.Round(sineValue)) - 1 + + // Store the value in the table + table[i] = roundedValue + } + + return table +} diff --git a/tmc5160/spicomm.go b/tmc5160/spicomm.go new file mode 100644 index 0000000..4127f0f --- /dev/null +++ b/tmc5160/spicomm.go @@ -0,0 +1,134 @@ +//go:build tinygo + +package tmc5160 + +import ( + "machine" + "time" +) + +// CustomError is a lightweight error type used for TinyGo compatibility. +type CustomError string + +func (e CustomError) Error() string { + return string(e) +} + +// SPIComm implements RegisterComm for SPI-based communication +type SPIComm struct { + spi machine.SPI + CsPins map[uint8]machine.Pin // Map to store CS pin for each Driver by its address +} + +// NewSPIComm creates a new SPIComm instance. +func NewSPIComm(spi machine.SPI, csPins map[uint8]machine.Pin) *SPIComm { + return &SPIComm{ + spi: spi, + CsPins: csPins, + } +} + +// Setup initializes the SPI communication with the Driver and configures all CS pins. +func (comm *SPIComm) Setup() error { + // Check if SPI is initialized + if comm.spi == (machine.SPI{}) { + return CustomError("SPI not initialized") + } + + // Configure all CS pins (make them output and set them high) + for _, csPin := range comm.CsPins { + csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + csPin.High() // Set all CS pins high initially + } + + // Configure the SPI interface with the desired settings + err := comm.spi.Configure(machine.SPIConfig{ + LSBFirst: false, + Mode: 3, + }) + if err != nil { + return CustomError("Failed to configure SPI") + } + + return nil +} + +// WriteRegister sends a register write command to the TMC5160. +func (comm *SPIComm) WriteRegister(register uint8, value uint32, driverAddress uint8) error { + // Assert the chip select pin (set CS low to start communication) + csPin, exists := comm.CsPins[driverAddress] + if !exists { + return CustomError("Invalid driver address") + } + csPin.Low() + + // Set the register address with WRITE_ACCESS (0x80) + addressWithWriteAccess := register | 0x80 + + // Send the address and the data to write (split into 4 bytes) + _, err := spiTransfer40(&comm.spi, addressWithWriteAccess, value) + if err != nil { + csPin.High() + return CustomError("Failed to write register") + } + + // Deassert the chip select pin (set CS high to end communication) + csPin.High() + + return nil +} + +// ReadRegister sends a register read command to the TMC5160. +func (comm *SPIComm) ReadRegister(register uint8, driverAddress uint8) (uint32, error) { + // Assert the chip select pin (set CS low to start communication) + csPin, exists := comm.CsPins[driverAddress] + if !exists { + return 0, CustomError("Invalid driver address") + } + csPin.Low() + + // Step 1: Send a dummy write operation to begin the read sequence + _, err := spiTransfer40(&comm.spi, register, 0x00) // Send dummy data + if err != nil { + csPin.High() + return 0, CustomError("Failed to send dummy write") + } + csPin.High() + time.Sleep(176 * time.Nanosecond) + csPin.Low() + // Step 2: Send the register read request again to get the actual value + response, err := spiTransfer40(&comm.spi, register, 0x00) // Send again to get actual register data + if err != nil { + csPin.High() + return 0, CustomError("Failed to read register") + } + + // Deassert the chip select pin (set CS high to end communication) + csPin.High() + + return response, nil +} + +func spiTransfer40(spi *machine.SPI, register uint8, txData uint32) (uint32, error) { + // Prepare the 5-byte buffer for transmission (1 byte address + 4 bytes data) + tx := []byte{ + register, // Address byte + byte(txData >> 24), // Upper 8 bits of data + byte(txData >> 16), // Middle 8 bits of data + byte(txData >> 8), // Next 8 bits of data + byte(txData), // Lower 8 bits of data + } + + rx := make([]byte, 5) + + // Perform the SPI transaction + err := spi.Tx(tx, rx) + if err != nil { + return 0, err + } + + // Combine the received bytes into a 32-bit response, ignore the address byte + rxData := uint32(rx[1])<<24 | uint32(rx[2])<<16 | uint32(rx[3])<<8 | uint32(rx[4]) + + return rxData, nil +} diff --git a/tmc5160/stepper.go b/tmc5160/stepper.go new file mode 100644 index 0000000..62e1bde --- /dev/null +++ b/tmc5160/stepper.go @@ -0,0 +1,105 @@ +package tmc5160 + +const maxVMAX = 8388096 + +// PowerStageParameters represents the power stage parameters +type PowerStageParameters struct { + drvStrength uint8 + bbmTime uint8 + bbmClks uint8 +} + +// MotorParameters represents the motor parameters +type MotorParameters struct { + globalScaler uint16 + ihold uint8 + irun uint8 + iholddelay uint8 + pwmGradInitial uint16 + pwmOfsInitial uint16 + freewheeling uint8 +} + +// MotorDirection defines motor direction constants +type MotorDirection uint8 + +const ( + Clockwise MotorDirection = iota + CounterClockwise +) + +const ( + // Common stepper motor angles + StepAngle_1_8 = 1.8 + StepAngle_0_9 = 0.9 + StepAngle_0_72 = 0.72 + StepAngle_1_2 = 1.2 + StepAngle_0_48 = 0.48 + + // Common microstepping options + Step_1 uint8 = 1 + Step_2 uint8 = 2 + Step_4 uint8 = 4 + Step_8 uint8 = 8 + Step_16 uint8 = 16 + Step_32 uint8 = 32 + Step_64 uint8 = 64 + Step_128 uint8 = 128 +) + +const ( + DefaultAngle float32 = StepAngle_1_8 + DefaultGearRatio float32 = 1.0 + DefaultVSupply float32 = 12.0 + DefaultRCoil float32 = 1.2 + DefaultLCoil float32 = 0.005 + DefaultIPeak float32 = 2.0 + DefaultRSense float32 = 0.1 + DefaultFclk uint8 = 12 + DefaultStep_256 = 256 +) + +type Stepper struct { + Angle float32 + GearRatio float32 + VelocitySPS float32 // Velocity in Steps per sec + VSupply float32 + RCoil float32 + LCoil float32 + IPeak float32 + RSense float32 + MSteps uint8 + Fclk uint8 //Clock in Mhz + +} + +// NewStepper function initializes a Stepper with default values used for testing +func NewDefaultStepper() Stepper { + return Stepper{ + Angle: StepAngle_1_8, // Default to 1.8 degrees + GearRatio: 1.0, // Default to no reduction (1:1) + VSupply: 12.0, // Default 12V supply + RCoil: 1.2, // Default coil resistance (1.2 ohms) + LCoil: 0.005, // Default coil inductance (5 mH) + IPeak: 2.0, // Default peak current (2A) + RSense: 0.1, // Default sense resistance (0.1 ohms) + MSteps: Step_16, // Default 16 Microsteps + Fclk: DefaultFclk, + } +} + +// NewStepper initializes a Stepper with user-defined values +func NewStepper(angle float32, gearRatio, vSupply, rCoil, lCoil, iPeak, rSense float32, mSteps uint8, fclk uint8) Stepper { + return Stepper{ + Angle: angle, // User-defined stepper angle (e.g., StepAngle_1_8) + GearRatio: gearRatio, // User-defined gear ratio + VSupply: vSupply, // User-defined supply voltage + RCoil: rCoil, // User-defined coil resistance + LCoil: lCoil, // User-defined coil inductance + IPeak: iPeak, // User-defined peak current + RSense: rSense, // User-defined sense resistance + MSteps: mSteps, // User-defined microstepping setting + Fclk: fclk, // User-defined clock frequency in MHz + + } +} diff --git a/tmc5160/tmc5160.go b/tmc5160/tmc5160.go new file mode 100644 index 0000000..0bf2b10 --- /dev/null +++ b/tmc5160/tmc5160.go @@ -0,0 +1,194 @@ +//go:build tinygo + +package tmc5160 + +import ( + "github.com/orsinium-labs/tinymath" + "machine" +) + +type Driver struct { + comm RegisterComm + address uint8 + enablePin machine.Pin + stepper Stepper +} + +func NewDriver(comm RegisterComm, address uint8, enablePin machine.Pin, stepper Stepper) *Driver { + return &Driver{ + comm: comm, + address: address, + enablePin: enablePin, + stepper: stepper, + } +} + +// WriteRegister sends a register write command to the Driver. +func (driver *Driver) WriteRegister(reg uint8, value uint32) error { + if driver.comm == nil { + return CustomError("communication interface not set") + } + // Use the communication interface (RegisterComm) to write the register + return driver.comm.WriteRegister(reg, value, driver.address) +} + +// ReadRegister sends a register read command to the Driver and returns the read value. +func (driver *Driver) ReadRegister(reg uint8) (uint32, error) { + if driver.comm == nil { + return 0, CustomError("communication interface not set") + } + // Use the communication interface (RegisterComm) to read the register + return driver.comm.ReadRegister(reg, driver.address) +} + +// Begin initializes the Driver driver with power and motor parameters +func (driver *Driver) Begin(powerParams PowerStageParameters, motorParams MotorParameters, stepperDirection MotorDirection) bool { + // Clear the reset and charge pump undervoltage flags + gstat := NewGSTAT() + gstat.Reset = true + gstat.UvCp = true + err := driver.WriteRegister(GSTAT, gstat.Pack()) + if err != nil { + return false + } + + // Configure driver settings + drvConf := NewDRV_CONF() + drvConf.DrvStrength = constrain(powerParams.drvStrength, 0, 3) + drvConf.BBMTime = constrain(powerParams.bbmTime, 0, 24) + drvConf.BBMClks = constrain(powerParams.bbmClks, 0, 15) + err = driver.WriteRegister(DRV_CONF, drvConf.Pack()) + if err != nil { + return false + } + + // Set global scaler + err = driver.WriteRegister(GLOBAL_SCALER, uint32(constrain(motorParams.globalScaler, 32, 256))) + if err != nil { + return false + } + + // Set initial currents and delay + iholdrun := NewIHOLD_IRUN() + iholdrun.Ihold = constrain(motorParams.ihold, 0, 31) + iholdrun.Ihold = constrain(motorParams.irun, 0, 31) + iholdrun.IholdDelay = 7 + err = driver.WriteRegister(IHOLD_IRUN, iholdrun.Pack()) + if err != nil { + return false + } + + // Set PWM configuration values + pwmconf := NewPWMCONF() + err = driver.WriteRegister(PWMCONF, 0xC40C001E) + if err != nil { + return false + } // Reset default value pwm_ofs = 196,pwm_grad = 12,pwm_freq = 0, pwm_autoscale = false, pwm_autograd = false,freewheel = 3 + pwmconf.PwmAutoscale = false // Temporarily set to false for setting OFS and GRAD values + _fclk := int(driver.stepper.Fclk) * 1000000 + if _fclk > DEFAULT_F_CLK { + pwmconf.PwmFreq = 0 + } else { + pwmconf.PwmFreq = 0b01 // Recommended: 35kHz with internal 12MHz clock + } + pwmconf.PwmGrad = uint8(motorParams.pwmGradInitial) + pwmconf.PwmOfs = uint8(motorParams.pwmOfsInitial) + pwmconf.Freewheel = motorParams.freewheeling + err = driver.WriteRegister(PWMCONF, pwmconf.Pack()) + if err != nil { + return false + } + + // Enable PWM auto-scaling and gradient adjustment + pwmconf.PwmAutoscale = true + pwmconf.PwmAutograd = true + err = driver.WriteRegister(PWMCONF, pwmconf.Pack()) + if err != nil { + return false + } + + // Recommended chop configuration settings + _chopConf := NewCHOPCONF() + _chopConf.Toff = 5 + _chopConf.Tbl = 2 + _chopConf.HstrtTfd = 4 + _chopConf.HendOffset = 0 + err = driver.WriteRegister(CHOPCONF, _chopConf.Pack()) + if err != nil { + return false + } + rampMode := NewRAMPMODE(driver.comm, driver.address) + rampMode.SetMode(PositioningMode) + gconf := NewGCONF() + gconf.EnPwmMode = true // Enable stealthChop PWM mode + gconf.Shaft = stepperDirection == Clockwise + err = driver.WriteRegister(GCONF, gconf.Pack()) + if err != nil { + return false + } + + // Set default start, stop, threshold speeds + driver.setRampSpeeds(0.0, 0.1, 0.0) // Start, stop, threshold speeds + + // Set default D1 (must not be = 0 in positioning mode even with V1=0) + err = driver.WriteRegister(D_1, 100) + if err != nil { + return false + } + + return false +} +func (driver *Driver) setRampSpeeds(startSpeed float32, stopSpeed float32, transitionSpeed float32) { + str := driver.stepper.DesiredSpeedToTSTEP(uint32(startSpeed)) + stp := driver.stepper.DesiredSpeedToTSTEP(uint32(stopSpeed)) + ts := driver.stepper.DesiredSpeedToTSTEP(uint32(transitionSpeed)) + driver.WriteRegister(VSTART, uint32(tinymath.Min(0x3FFFF, float32(str)))) + driver.WriteRegister(VSTOP, uint32(tinymath.Min(0x3FFFF, float32(stp)))) + driver.WriteRegister(V_1, uint32(tinymath.Min(0xFFFFF, float32(ts)))) + println("Ramp set to: startSpeed:", startSpeed, "stopSpeed:", stopSpeed, "transitionSpeed:", transitionSpeed) +} + +// setMaxSpeed sets the maximum speed to 0 (placeholder function) +func setMaxSpeed(speed uint32) { + // This is a placeholder function that sets the speed + // Implement the actual logic to set the maximum speed register value + println("Max Speed set to:", speed) +} + +// Dump_TMC reads multiple registers from the Driver and logs their values with their names. +func (driver *Driver) Dump_TMC() error { + registers := []uint8{ + GCONF, CHOPCONF, GSTAT, DRV_STATUS, FACTORY_CONF, IOIN, LOST_STEPS, MSCNT, + MSCURACT, OTP_READ, PWM_SCALE, PWM_AUTO, TSTEP, + } + registerNames := map[uint8]string{ + GCONF: "GCONF", + CHOPCONF: "CHOPCONF", + GSTAT: "GSTAT", + DRV_STATUS: "DRV_STATUS", + FACTORY_CONF: "FACTORY_CONF", + IOIN: "IOIN", + LOST_STEPS: "LOST_STEPS", + MSCNT: "MSCNT", + MSCURACT: "MSCURACT", + OTP_READ: "OTP_READ", + PWM_SCALE: "PWM_SCALE", + PWM_AUTO: "PWM_AUTO", + TSTEP: "TSTEP", + } + for _, reg := range registers { + // Fetch the register name from the map + regName, exists := registerNames[reg] + if !exists { + regName = "Unknown Register" + } + val, err := driver.ReadRegister(reg) + if err != nil { + println("Error reading register", regName, err) + return err + } + println("Register", regName, "Value:", val) + } + + return nil +} diff --git a/tmc5160/uartcomm.go b/tmc5160/uartcomm.go new file mode 100644 index 0000000..37321a5 --- /dev/null +++ b/tmc5160/uartcomm.go @@ -0,0 +1,101 @@ +//go:build uart + +package tmc5160 + +import ( + "machine" + "time" +) + +// UARTComm implements RegisterComm for UART-based communication with Driver. +type UARTComm struct { + uart machine.UART + address uint8 +} + +// NewUARTComm creates a new UARTComm instance. +func NewUARTComm(uart machine.UART, address uint8) *UARTComm { + return &UARTComm{ + uart: uart, + address: address, + } +} + +// Setup initializes the UART communication with the Driver. +func (comm *UARTComm) Setup() error { + if comm.uart == (machine.UART{}) { + return CustomError("UART not initialized") + } + err := comm.uart.Configure(machine.UARTConfig{ + BaudRate: 115200, + }) + if err != nil { + return CustomError("Failed to configure UART") + } + return nil +} + +// WriteRegister sends a register write command to the Driver. +// Prepare the data packet (sync byte + address + register + data + checksum) +func (comm *UARTComm) WriteRegister(register uint8, value uint32, driverIndex uint8) error { + + buffer := []byte{ + 0x05, // Sync byte + comm.address, // Slave address + register | 0x80, // Write command (MSB set to 1 for write) + byte((value >> 24) & 0xFF), // MSB of value + byte((value >> 16) & 0xFF), // Middle byte + byte((value >> 8) & 0xFF), // Next byte + byte(value & 0xFF), // LSB of value + } + checksum := byte(0) + for _, b := range buffer[:7] { + checksum ^= b + } + buffer[7] = checksum // Set checksum byte + + // Write the data to the Driver + done := make(chan error, 1) + + go func() { + comm.uart.Write(buffer) + done <- nil + }() + + select { + case err := <-done: + return err + case <-time.After(100 * time.Millisecond): // Timeout after 100ms + return CustomError("write timeout") + } +} + +// ReadRegister sends a register read command to the Driver. +func (comm *UARTComm) ReadRegister(register uint8, driverIndex uint8) (uint32, error) { + // Prepare the read command (sync byte + address + register + checksum) + var writeBuffer [4]byte + writeBuffer[0] = 0x05 // Sync byte + writeBuffer[1] = comm.address // Slave address + writeBuffer[2] = register & 0x7F // Read command (MSB clear for read) + writeBuffer[3] = writeBuffer[0] ^ writeBuffer[1] ^ writeBuffer[2] // Checksum + done := make(chan []byte, 1) + go func() { + comm.uart.Write(writeBuffer[:]) + readBuffer := make([]byte, 8) // Prepare the buffer to read 8 bytes + comm.uart.Read(readBuffer) + done <- readBuffer + }() + select { + case readBuffer := <-done: + checksum := byte(0) + for i := 0; i < 7; i++ { + checksum ^= readBuffer[i] + } + if checksum != readBuffer[7] { + return 0, CustomError("checksum error") + } + return uint32(readBuffer[3])<<24 | uint32(readBuffer[4])<<16 | uint32(readBuffer[5])<<8 | uint32(readBuffer[6]), nil + case <-time.After(100 * time.Millisecond): // Timeout after 100ms + return 0, CustomError("read timeout") + } +} diff --git a/tmc5160/utils.go b/tmc5160/utils.go new file mode 100644 index 0000000..ce2dade --- /dev/null +++ b/tmc5160/utils.go @@ -0,0 +1,13 @@ +package tmc5160 + +func ToHex(value uint32) string { + hexChars := "0123456789ABCDEF" + result := "" + + for i := 0; i < 8; i++ { // 8 nibbles for a 32-bit number + nibble := (value >> (28 - i*4)) & 0xF + result += string(hexChars[nibble]) + } + + return "0x" + result +} From f57b5ecee91cee28663b5d77c62bb07355fd2457 Mon Sep 17 00:00:00 2001 From: Anastasios Papalyras Date: Tue, 28 Jan 2025 13:17:12 +0200 Subject: [PATCH 054/137] Add sharpmem (#724) sharpmem: add implementation of sharpmem display driver * Implement Configure, Clear and ClearBuffer, add some tests, add documentation/comments * Reverse white * Inverted bits, fix and improve ClearBuffer, cleanup * Refine doc comment * Driver refactor, optimizations toggle, additional tests & support for all SKUs * Fix address overflow padding, add wire-level tests for assumed address encoding * Minor rename * Cleanup and doc fixes * Add device configs * Bounds check * Add example and smoketest entry * Use uf2 output file format * Refine example --- examples/sharpmem/main.go | 90 +++++++++ sharpmem/sharpmem.go | 374 ++++++++++++++++++++++++++++++++++++++ sharpmem/sharpmem_test.go | 225 +++++++++++++++++++++++ sharpmem/util.go | 30 +++ smoketest.sh | 1 + 5 files changed, 720 insertions(+) create mode 100644 examples/sharpmem/main.go create mode 100644 sharpmem/sharpmem.go create mode 100644 sharpmem/sharpmem_test.go create mode 100644 sharpmem/util.go diff --git a/examples/sharpmem/main.go b/examples/sharpmem/main.go new file mode 100644 index 0000000..2a3f2d7 --- /dev/null +++ b/examples/sharpmem/main.go @@ -0,0 +1,90 @@ +package main + +import ( + "image/color" + "machine" + "math/rand/v2" + "time" + + "tinygo.org/x/drivers/sharpmem" +) + +var ( + // example wiring using a nice!view and nice!nano: + // (view) (nano) + // MOSI --> P0.24 + // SCK ---> P0.22 + // GND ---> GND + // VCC ---> 3.3V + // CS ----> P0.06 + + spi = machine.SPI0 + + sckPin = machine.SPI0_SCK_PIN // SCK + sdoPin = machine.SPI0_SDO_PIN // MOSI + sdiPin = machine.SPI0_SDI_PIN // (any pin) + + csPin = machine.P0_06 // CS +) + +func main() { + time.Sleep(time.Second) + + err := spi.Configure(machine.SPIConfig{ + Frequency: 2000000, + SCK: sckPin, + SDO: sdoPin, + SDI: sdiPin, + Mode: 0, + LSBFirst: true, + }) + if err != nil { + println("spi.Configure() failed, error:", err.Error()) + return + } + + csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + display := sharpmem.New(spi, csPin) + + cfg := sharpmem.ConfigLS011B7DH03 + display.Configure(cfg) + + // clear the display before first use + err = display.Clear() + if err != nil { + println("display.Clear() failed, error:", err.Error()) + return + } + + // random boxes pop into and out of existence + for { + x0 := int16(rand.IntN(int(cfg.Width - 7))) + y0 := int16(rand.IntN(int(cfg.Height - 7))) + + for x2 := int16(0); x2 < 16; x2++ { + x2 := x2 + c := color.RGBA{R: 255, G: 255, B: 255, A: 255} + + if x2 >= 8 { + // effectively erases the box after it showed up + x2 = x2 - 8 + c = color.RGBA{R: 0, G: 0, B: 0, A: 255} + } + + for x := int16(0); x < x2; x++ { + for y := int16(0); y < 8; y++ { + display.SetPixel(x0+x, y0+y, c) + } + } + + err = display.Display() + if err != nil { + println("display.Display() failed, error:", err.Error()) + continue + } + + time.Sleep(33 * time.Millisecond) + } + } +} diff --git a/sharpmem/sharpmem.go b/sharpmem/sharpmem.go new file mode 100644 index 0000000..45f965b --- /dev/null +++ b/sharpmem/sharpmem.go @@ -0,0 +1,374 @@ +package sharpmem + +import ( + "errors" + "image/color" + + "tinygo.org/x/drivers" +) + +const ( + bitWriteCmd uint8 = 0b00000001 + bitVcom uint8 = 0b00000010 + bitClear uint8 = 0b00000100 +) + +var ( + ConfigLS010B7DH04 = Config{Width: 128, Height: 128} + ConfigLS011B7DH03 = Config{Width: 160, Height: 68} + ConfigLS012B7DD01 = Config{Width: 184, Height: 38} + ConfigLS013B7DH03 = ConfigLS010B7DH04 + ConfigLS013B7DH05 = Config{Width: 144, Height: 168} + ConfigLS018B7DH02 = Config{Width: 230, Height: 303} + ConfigLS027B7DH01 = Config{Width: 400, Height: 240} + ConfigLS027B7DH01A = ConfigLS027B7DH01 + ConfigLS032B7DD02 = Config{Width: 336, Height: 536} + ConfigLS044Q7DH01 = Config{Width: 320, Height: 240} +) + +type Pin interface { + High() + Low() +} + +// Device represents a Sharp Memory Display device. This driver implementation +// concerns the 1-bit color versions only (black and white memory displays). +// +// Supported SKUs include: +// LS010B7DH04, LS011B7DH03, LS012B7DD01, LS013B7DH03, LS013B7DH05, +// LS018B7DH02, LS027B7DH01, LS027B7DH01A, LS032B7DD02, LS044Q7DH01 +// +// Note: Only SKU LS011B7DH03 (160x68) has been tested as of writing. +// +// The driver includes optimizations (frame and per-line invalidation) that +// only transmit the changed lines to the display. These optimizations are on +// by default, and they can be disabled with the respective config option. +type Device struct { + bus drivers.SPI + csPin Pin + buffer []byte + txBuf []byte + lineDiff []byte + width int16 + height int16 + bufferSize int16 + bytesPerLine int16 + vcom uint8 + diffing bool +} + +type Config struct { + Width int16 + Height int16 + + // DisableOptimizations disables frame and line invalidation optimizations. + // Useful if constant frame times are desired. + DisableOptimizations bool +} + +// New creates a new device connection. +// The SPI bus must have already been configured. +func New(bus drivers.SPI, csPin Pin) Device { + d := Device{ + bus: bus, + csPin: csPin, + } + return d +} + +// Configure initializes the display with specified configuration. It can be +// called multiple times on the same display, resetting its internal state. +func (d *Device) Configure(cfg Config) { + if cfg.Width == 0 { + cfg.Width = 160 + } + if cfg.Height == 0 { + cfg.Height = 68 + } + + d.width = cfg.Width + d.height = cfg.Height + d.diffing = !cfg.DisableOptimizations + + d.initialize() +} + +// initialize properly initializes the display and the in-memory image buffers. +func (d *Device) initialize() { + d.csPin.Low() + + // initialize VCOM as high + d.vcom = bitVcom + + // bytesPerLine has to be 16-bit aligned, as some resolutions require + // padding to the nearest 2nd byte. + d.bytesPerLine = ceilDiv(d.width, 16) * 2 + + // preallocate a contiguous byte buffer for all lines, including + // protocol-required padding for each line apriori (easier to transfer). + d.bufferSize = d.bytesPerLine * d.height + d.buffer = make([]byte, d.bufferSize) + // A bit being 1 is white (reflective), 0 is black (less reflective). + for i := range d.buffer { + d.buffer[i] = 0xff + } + + // auxiliary buffer for SPI transfers to avoid dynamic allocations + d.txBuf = make([]byte, 2) + + if d.diffing { + // buffer to store the changed lines. First bit is whether any line has + // changed at all (i.e. the frame is invalid), followed by N bits, + // one for each line. + d.lineDiff = make([]byte, bitfieldBufLen(1+d.height)) + } +} + +// SetPixel enables or disables a pixel in the buffer. +// color.RGBA{0, 0, 0, 255} is considered transparent (reflective, white), +// anything else will enable a pixel on the screen (make it appear less +// reflective, black). +func (d *Device) SetPixel(x, y int16, c color.RGBA) { + if d.width == 0 { + return + } + + // bounds check + if x < 0 || x >= d.width || y < 0 || y >= d.height { + return + } + + offset := y * d.bytesPerLine + + div := offset + x/8 + mod := uint8(x % 8) + + prev := hasBit(d.buffer[div], mod) + curr := c.R == 0 && c.G == 0 && c.B == 0 && c.A == 255 + + if prev == curr { + return + } + + if curr { + d.buffer[div] = setBit(d.buffer[div], mod) + } else { + d.buffer[div] = unsetBit(d.buffer[div], mod) + } + + if d.diffing { + d.invalidateLine(y) + } +} + +// Size returns the current size of the display. +func (d *Device) Size() (x, y int16) { + return d.width, d.height +} + +// Display renders the buffer to the screen. It only transmits changed lines if +// optimizations are enabled. It should be called at >=1hz, even if the +// buffer hasn't been modified. +func (d *Device) Display() error { + if d.width == 0 { + return errors.New("display not configured") + } + + if d.diffing { + if !hasBit(d.lineDiff[0], 0) { + // no pixels have been modified, simply toggle VCOM + return d.holdDisplay() + } + + defer func() { + for i := 0; i < len(d.lineDiff); i++ { + d.lineDiff[i] = 0x00 + } + }() + } + + cmd := bitWriteCmd | d.vcom + + d.toggleVcom() + + // Padding to use for high bits of line numbers that overflow 8 bits. + var hiPad = uint8(0) + if d.height >= 512 { + hiPad = 3 + 3 // 3 mode bits + 3 low bits + } else if d.height >= 256 { + hiPad = 3 + 4 // 3 mode bits + 4 low bits + } + + // start transfer + d.csPin.High() + + for i := int16(0); i < d.height; i++ { + if d.diffing { + // Skip rendering lines that haven't changed. + linediv := (i + 1) / 8 + linemod := uint8((i + 1) % 8) + if !hasBit(d.lineDiff[linediv], linemod) { + continue + } + } + + // The first 5 bits are either dummy or part of the current line + // (1-indexed) if it overflows 8-bits. + // The last 3 bits are the command for the first line and dummy bits + // for subsequent lines (set as command for simplicity) + hi := uint8((i + 1) >> 8) + hi = hi << hiPad + d.txBuf[0] = cmd | hi + + // The second byte is the low bits of the current line (1-indexed). + // for <8 bits cases, the high bits are dummy, so we leave them as 0. + d.txBuf[1] = uint8(i + 1) + + // send the first two bytes + err := d.bus.Tx(d.txBuf, nil) + if err != nil { + return err + } + + // send the line data + err = d.bus.Tx(d.buffer[i*d.bytesPerLine:(i+1)*d.bytesPerLine], nil) + if err != nil { + return err + } + } + + // Trailer 16 bits (low) + d.txBuf[0] = 0x00 + d.txBuf[1] = 0x00 + err := d.bus.Tx(d.txBuf, nil) + if err != nil { + return err + } + + // end transfer + d.csPin.Low() + + return nil +} + +// holdDisplay simply toggles VCOM without updating any lines. +func (d *Device) holdDisplay() error { + d.txBuf[0] = d.vcom + d.txBuf[1] = 0x00 + + d.toggleVcom() + + // begin transaction + d.csPin.High() + + err := d.bus.Tx(d.txBuf, nil) + if err != nil { + return err + } + + // end transaction + d.csPin.Low() + + return nil +} + +// Clear clears both the in-memory buffer and the display. +func (d *Device) Clear() error { + if d.width == 0 { + return errors.New("display not configured") + } + + d.ClearBuffer() + return d.ClearDisplay() +} + +// ClearBuffer clears the in-memory buffer. The display is not updated. +func (d *Device) ClearBuffer() { + if d.width == 0 { + return + } + + if d.diffing { + // detect what rows need to be reset on the next render + d.invalidateModifiedLines() + } + + // reset the in-memory buffer + for i := 0; i < len(d.buffer); i++ { + d.buffer[i] = 0xff + } +} + +// invalidateModifiedLines marks any line that has at least a single black pixel +// as invalidated. Padding bits, if any, are always 1. +func (d *Device) invalidateModifiedLines() { + for y := int16(0); y < d.height; y++ { + offset := y * d.bytesPerLine + + updateLine := false + for x := int16(0); x < d.width; x++ { + div := offset + x/8 + mod := uint8(x % 8) + + if !hasBit(d.buffer[div], mod) { + updateLine = true + break + } + } + + if updateLine { + d.invalidateLine(y) + } + } +} + +// ClearDisplay clears the display. The in-memory buffer is not updated. A +// subsequent call to Display() will re-render the content as it was before +// clearing. +func (d *Device) ClearDisplay() error { + if d.width == 0 { + return errors.New("display not configured") + } + + d.txBuf[0] = d.vcom | bitClear + d.txBuf[1] = 0x00 + + d.toggleVcom() + + // begin transaction + d.csPin.High() + + err := d.bus.Tx(d.txBuf, nil) + if err != nil { + return err + } + + // end transaction + d.csPin.Low() + + return nil +} + +// invalidateLine marks a line and the frame itself as invalidated. +func (d *Device) invalidateLine(line int16) { + // mark the frame as invalidated + d.lineDiff[0] = setBit(d.lineDiff[0], 0) + + // mark the line as invalidated + linediv := (line + 1) / 8 + linemod := uint8((line + 1) % 8) + d.lineDiff[linediv] = setBit(d.lineDiff[linediv], linemod) +} + +// toggleVcom toggles the VCOM, as is instructed by the datasheet. +// Toggling VCOM can help maintain the display's longevity. It should ideally +// be called at least once per second, preferably at 4-100 Hz. +// Toggling VCOM causes a tiny bit of flicker, but without it the pixels can +// be permanently damaged by the DC bias accumulating over time. +func (d *Device) toggleVcom() { + if d.vcom != 0 { + d.vcom = 0x00 + } else { + d.vcom = bitVcom + } +} diff --git a/sharpmem/sharpmem_test.go b/sharpmem/sharpmem_test.go new file mode 100644 index 0000000..43a9645 --- /dev/null +++ b/sharpmem/sharpmem_test.go @@ -0,0 +1,225 @@ +package sharpmem + +import ( + "image/color" + "math/rand/v2" + "testing" + + qt "github.com/frankban/quicktest" +) + +func Test_setBit(t *testing.T) { + c := qt.New(t) + + for i := uint8(0); i < 8; i++ { + v := uint8(1) << i + + c.Assert(setBit(0x00, i), qt.Equals, v) + c.Assert(setBit(0x00, (i+1)%8), qt.Not(qt.Equals), v) + } +} + +func Test_unsetBit(t *testing.T) { + c := qt.New(t) + + for i := uint8(0); i < 8; i++ { + v := uint8(1) << i + + c.Assert(unsetBit(v, i), qt.Equals, uint8(0x00)) + c.Assert(unsetBit(v, (i+1)%8), qt.Not(qt.Equals), uint8(0x00)) + } +} + +func Test_hasBit(t *testing.T) { + c := qt.New(t) + + for i := uint8(0); i < 8; i++ { + v := uint8(1) << i + + c.Assert(hasBit(v, i), qt.Equals, true) + c.Assert(hasBit(v, (i+1)%8), qt.Equals, false) + } +} + +func Test_bitfieldBufLen(t *testing.T) { + c := qt.New(t) + + for i := int16(1); i < 536; i++ { + requiredBufferSize := i / 8 + wouldOverflow := i % 8 + + if wouldOverflow > 0 { + requiredBufferSize += 1 + } + + c.Assert(bitfieldBufLen(i), qt.Equals, requiredBufferSize) + } +} + +type mockBus struct { + b []byte +} + +func (m *mockBus) Tx(w, _ []byte) error { + m.b = append(m.b, w...) + return nil +} + +func (m *mockBus) Transfer(b byte) (byte, error) { + m.b = append(m.b, b) + return 0x00, nil +} + +type mockPin struct{} + +func (m mockPin) High() { +} + +func (m mockPin) Low() { +} + +func Test_Device(t *testing.T) { + c := qt.New(t) + + cfgs := []Config{ + ConfigLS010B7DH04, + ConfigLS011B7DH03, + ConfigLS012B7DD01, + ConfigLS013B7DH03, + ConfigLS013B7DH05, + ConfigLS018B7DH02, + ConfigLS027B7DH01, + ConfigLS027B7DH01A, + ConfigLS032B7DD02, + ConfigLS044Q7DH01, + } + + cfgLen := len(cfgs) + for i := 0; i < cfgLen; i++ { + cfgs = append(cfgs, Config{ + Width: cfgs[i].Width, + Height: cfgs[i].Height, + DisableOptimizations: true, + }) + } + + spi := &mockBus{} + pin := mockPin{} + display := New(spi, pin) + + for _, cfg := range cfgs { + display.Configure(cfg) + + x, y := display.Size() + c.Assert(x, qt.Equals, cfg.Width) + c.Assert(y, qt.Equals, cfg.Height) + + for i := 0; i < 10; i++ { + x := int16(rand.IntN(int(cfg.Width))) + y := int16(rand.IntN(int(cfg.Height))) + display.SetPixel(x, y, color.RGBA{R: 255, G: 255, B: 255, A: 255}) + } + + for i := 0; i < 10; i++ { + x := int16(rand.IntN(int(cfg.Width))) + y := int16(rand.IntN(int(cfg.Height))) + display.SetPixel(x, y, color.RGBA{R: 0, G: 0, B: 0, A: 255}) + } + + err := display.Display() + c.Assert(err, qt.Equals, nil) + + err = display.ClearDisplay() + c.Assert(err, qt.Equals, nil) + + display.ClearBuffer() + } +} + +func Test_HiPad(t *testing.T) { + c := qt.New(t) + + spi := &mockBus{} + pin := mockPin{} + display := New(spi, pin) + + t.Run("LS011B7DH03, 8-bit address", func(t *testing.T) { + t.Cleanup(func() { + spi.b = nil + }) + + display.Configure(ConfigLS011B7DH03) + + display.SetPixel(0, display.height-1, color.RGBA{R: 255, G: 255, B: 255, A: 255}) + err := display.Display() + c.Assert(err, qt.Equals, nil) + + // 160 perfectly divisible by 16, so 20 bytes of pixel data + c.Assert(spi.b, qt.HasLen, 2+20+2) + + // line is 1-indexed on the wire (67+1) + // 68 in binary + // 0b01000100 + + // DDDDDMMM + c.Assert(spi.b[0], qt.Equals, uint8(0b00000011)) // mode 1, vcom is high on first run + + c.Assert(spi.b[1], qt.Equals, uint8(0b01000100)) // the actual address + }) + + t.Run("LS018B7DH02, 9-bit address", func(t *testing.T) { + t.Cleanup(func() { + spi.b = nil + }) + + display.Configure(ConfigLS018B7DH02) + + display.SetPixel(0, display.height-1, color.RGBA{R: 255, G: 255, B: 255, A: 255}) + err := display.Display() + c.Assert(err, qt.Equals, nil) + + // 2 first bytes command+address + // 230 bits are not divisible by 16, 240 is (15*16), so 30 bytes for line data + // 2 trailing bytes + c.Assert(spi.b, qt.HasLen, 2+30+2) + + // line is 1-indexed on the wire (302+1) + // 303 in binary (split in 2 bytes) + // R + // 0b00000001 0b00101111 + // ^ + + // RDDDDMMM + c.Assert(spi.b[0], qt.Equals, uint8(0b10000011)) // mode 1, vcom is high on first run + // ^ + + c.Assert(spi.b[1], qt.Equals, uint8(0b00101111)) // rest of the address (low 8 bits) + }) + + t.Run("LS032B7DD02, 10-bit address", func(t *testing.T) { + t.Cleanup(func() { + spi.b = nil + }) + + display.Configure(ConfigLS032B7DD02) + + display.SetPixel(0, display.height-1, color.RGBA{R: 255, G: 255, B: 255, A: 255}) + err := display.Display() + c.Assert(err, qt.Equals, nil) + + c.Assert(spi.b, qt.HasLen, 2+336/8+2) // 2 command+address, width / 2, 2 trailing bytes + + // line is 1-indexed on the wire (535+1) + // 536 in binary (split in 2 bytes) + // RR + // 0b00000010 0b00011000 + // ^^ + + // RRDDDMMM + c.Assert(spi.b[0], qt.Equals, uint8(0b10000011)) // mode 1, vcom is high on first run + // ^^ + + c.Assert(spi.b[1], qt.Equals, uint8(0b00011000)) // rest of the address (low 8 bits) + }) + +} diff --git a/sharpmem/util.go b/sharpmem/util.go new file mode 100644 index 0000000..8c637da --- /dev/null +++ b/sharpmem/util.go @@ -0,0 +1,30 @@ +package sharpmem + +// setBit sets the bit at pos in n to 1 and returns the updated number. +func setBit(n uint8, pos uint8) uint8 { + n |= 1 << pos + return n +} + +// unsetBit sets the bit at pos in n to 0 and returns the updated number. +func unsetBit(n uint8, pos uint8) uint8 { + n &^= 1 << pos + return n +} + +// hasBit returns whether the bit at pos in n is 1. +func hasBit(n uint8, pos uint8) bool { + n = n & (1 << pos) + return n > 0 +} + +// bitfieldBufLen returns the required buffer size for keeping track of +// changed lines. +func bitfieldBufLen(bits int16) int16 { + return 1 + (bits-1)/8 +} + +// ceilDiv divides a with b, but it uses the ceiling if modulo is not 0. +func ceilDiv(a, b int16) int16 { + return 1 + (a-1)/b +} diff --git a/smoketest.sh b/smoketest.sh index 92b031d..ea74e86 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -137,6 +137,7 @@ tinygo build -size short -o ./build/test.hex -target=macropad-rp2040 ./examples/ tinygo build -size short -o ./build/test.hex -target=macropad-rp2040 ./examples/encoders/quadrature-interrupt tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mcp9808/main.go tinygo build -size short -o ./build/test.hex -target=pico ./examples/tmc5160/main.go +tinygo build -size short -o ./build/test.uf2 -target=nicenano ./examples/sharpmem/main.go # network examples (espat) tinygo build -size short -o ./build/test.hex -target=challenger-rp2040 ./examples/net/ntpclient/ # network examples (wifinina) From 87c205fd65f5a7e0aba320f4dadb152559f549fd Mon Sep 17 00:00:00 2001 From: Amken USA <166057890+amken3d@users.noreply.github.com> Date: Tue, 28 Jan 2025 06:42:18 -0500 Subject: [PATCH 055/137] Added TMC2209 support (#727) TMC2209: Added TMC2209 support --- examples/tmc2209/main.go | 35 + go.mod | 2 + smoketest.sh | 1 + tmc2209/README.md | 163 +++++ tmc2209/address.go | 1409 ++++++++++++++++++++++++++++++++++++++ tmc2209/current.go | 42 ++ tmc2209/motor_config.go | 37 + tmc2209/stealthchop.go | 25 + tmc2209/tmc2209.go | 56 ++ tmc2209/uartcomm.go | 120 ++++ tmc2209/utils.go | 73 ++ tmc2209/velocity.go | 12 + 12 files changed, 1975 insertions(+) create mode 100644 examples/tmc2209/main.go create mode 100644 tmc2209/README.md create mode 100644 tmc2209/address.go create mode 100644 tmc2209/current.go create mode 100644 tmc2209/motor_config.go create mode 100644 tmc2209/stealthchop.go create mode 100644 tmc2209/tmc2209.go create mode 100644 tmc2209/uartcomm.go create mode 100644 tmc2209/utils.go create mode 100644 tmc2209/velocity.go diff --git a/examples/tmc2209/main.go b/examples/tmc2209/main.go new file mode 100644 index 0000000..fdb74ba --- /dev/null +++ b/examples/tmc2209/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "machine" + + "tinygo.org/x/drivers/tmc2209" +) + +func main() { + uart := machine.UART0 + comm := tmc2209.NewUARTComm(*uart, 0) + // Create an instance of the TMC2209 with UART communication + tmc := tmc2209.NewTMC2209(comm, 0x00) // Replace 0x00 with the appropriate address + + // Set up the TMC2209 driver + err := tmc.Setup() + if err != nil { + println("Failed to set up TMC2209: ", err) + } + + // Write to a register (example: setting a register value) + err = tmc.WriteRegister(0x10, 0x12345678) // Replace 0x10 with the register address and 0x12345678 with the value + if err != nil { + println("Failed to write register:", err) + } + + // Read from a register (example: reading a register value) + value, err := tmc.ReadRegister(0x10) + if err != nil { + println("Failed to read register: ", err) + } + + // Output the read value + println("Register value: ", value) +} diff --git a/go.mod b/go.mod index 530f060..7762179 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,11 @@ module tinygo.org/x/drivers + go 1.22.1 toolchain go1.23.1 + require ( github.com/eclipse/paho.mqtt.golang v1.2.0 github.com/frankban/quicktest v1.10.2 diff --git a/smoketest.sh b/smoketest.sh index ea74e86..50a6440 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -136,6 +136,7 @@ tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mpu9150/mai tinygo build -size short -o ./build/test.hex -target=macropad-rp2040 ./examples/sh1106/macropad_spi tinygo build -size short -o ./build/test.hex -target=macropad-rp2040 ./examples/encoders/quadrature-interrupt tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mcp9808/main.go +tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/tmc2209/main.go tinygo build -size short -o ./build/test.hex -target=pico ./examples/tmc5160/main.go tinygo build -size short -o ./build/test.uf2 -target=nicenano ./examples/sharpmem/main.go # network examples (espat) diff --git a/tmc2209/README.md b/tmc2209/README.md new file mode 100644 index 0000000..7e67ede --- /dev/null +++ b/tmc2209/README.md @@ -0,0 +1,163 @@ +# TMC2209 Go Package + +This package provides a lightweight interface to communicate with the TMC2209 stepper motor driver via UART. It is designed to be used with microcontrollers and supports both default UART communication and custom interface implementations for flexibility. +Currently this package only implements the communications with TMC2209. Functions to handle specific operations such as EnableStealthChop() etc. will need to be implemented by the user. +## Features +- **UART Communication:** Standard communication through UART for controlling the TMC2209 driver. +- **Custom Communication Interfaces:** Allows custom implementations of communication interfaces (e.g., USB, SPI, etc.) via the `RegisterComm` interface. The usecase for this is if you have host system that can "talk" to the microcontroller connected to the TMC2209 +- **Error Handling:** Lightweight error handling for TinyGo compatibility. +- **Register Access:** Provides methods to read and write registers on the TMC2209. + +## Setup + +### Prerequisites +- **TinyGo**: This package is optimized for TinyGo, which is suitable for running Go on microcontrollers and embedded systems. +- **UART Communication**: For microcontrollers with UART support, such as ESP32, STM32, and Raspberry Pi Pico. + + +## Usage +### Using with Microcontrollers (Default UART) + +To use the package with a microcontroller, you need to initialize the UART communication and configure the TMC2209 driver. +Example: + +```aiignore +package main + +import ( + "fmt" + "log" + "github.com/yourusername/tmc2209" + "machine" +) + +func main() { + uart := machine.UART0 + + // Create an instance of the TMC2209 with UART communication + tmc := tmc2209.NewTMC2209(uart, 0x00) // Replace 0x00 with the appropriate address + + // Set up the TMC2209 driver + err := tmc.Setup() + if err != nil { + log.Fatalf("Failed to set up TMC2209: %v", err) + } + + // Write to a register (example: setting a register value) + err = tmc.WriteRegister(0x10, 0x12345678) // Replace 0x10 with the register address and 0x12345678 with the value + if err != nil { + log.Fatalf("Failed to write register: %v", err) + } + + // Read from a register (example: reading a register value) + value, err := tmc.ReadRegister(0x10) + if err != nil { + log.Fatalf("Failed to read register: %v", err) + } + + // Output the read value + fmt.Printf("Register value: 0x%X\n", value) +} + +``` +## Microcontroller Notes + +- **TinyGo Support:** This code is optimized for use with TinyGo on supported microcontrollers. +- **UART Configuration:** Ensure the UART instance is configured correctly for your microcontroller. +- The machine.UART0 in the example is for the default UART on TinyGo-compatible devices like the Raspberry Pi Pico. Check your microcontroller's documentation for the correct UART instance and pin configuration. + +## 2. Custom Interface Implementation + +If you want to use a custom communication interface (e.g., USB, SPI, etc.), you can implement the RegisterComm interface. +Custom Interface Example (e.g., USB Communication): + +``` +package main + +import ( + "fmt" + "log" + "github.com/yourusername/tmc2209" + "yourcustompackage" // Custom package for communication +) + +type CustomComm struct { + // Implement your custom communication method here +} + +func (c *CustomComm) ReadRegister(register uint8, driverIndex uint8) (uint32, error) { + // Implement the register read logic using your custom interface (USB, SPI, etc.) + // For example, send the register read command over USB and read the response + return 0, nil +} + +func (c *CustomComm) WriteRegister(register uint8, value uint32, driverIndex uint8) error { + // Implement the register write logic using your custom interface (USB, SPI, etc.) + // For example, send the register write command over USB + return nil +} + +func main() { + // Create an instance of the TMC2209 with your custom communication interface + customComm := &CustomComm{} + tmc := tmc2209.NewTMC2209(customComm, 0x00) // Replace 0x00 with the appropriate address + + // Set up the TMC2209 driver + err := tmc.Setup() + if err != nil { + log.Fatalf("Failed to set up TMC2209: %v", err) + } + + // Write to a register (example: setting a register value) + err = tmc.WriteRegister(0x10, 0x12345678) // Replace 0x10 with the register address and 0x12345678 with the value + if err != nil { + log.Fatalf("Failed to write register: %v", err) + } + + // Read from a register (example: reading a register value) + value, err := tmc.ReadRegister(0x10) + if err != nil { + log.Fatalf("Failed to read register: %v", err) + } + + // Output the read value + fmt.Printf("Register value: 0x%X\n", value) +} + +``` + +### Custom Interface Notes: + +- RegisterComm Interface: Your custom communication interface (e.g., USB, SPI) should implement the RegisterComm interface. This ensures that the TMC2209 driver can interact with your custom interface for reading and writing registers. +- Error Handling: Implement proper error handling in your custom interface methods (ReadRegister and WriteRegister). + +#### Functions +``` +NewTMC2209(comm RegisterComm, address uint8) *TMC2209 +``` +Creates a new TMC2209 instance with the provided communication interface (comm) and driver address (address). + +```Setup() error``` + +Initializes the communication interface. This is required before interacting with the TMC2209 driver. The method checks if the communication interface is UART and sets it up accordingly. + +```WriteRegister(register uint8, value uint32) error``` + +Writes a value to a specific register on the TMC2209 driver. + +```ReadRegister(register uint8) (uint32, error)``` + +Reads a value from a specific register on the TMC2209 driver. + +### Error Handling + +This package uses a lightweight custom error type (CustomError) to ensure compatibility with TinyGo and reduce external dependencies. +Example Error: + +```CustomError("communication interface not set")``` + + +Created by Amken3d +info@amken3d.us + + diff --git a/tmc2209/address.go b/tmc2209/address.go new file mode 100644 index 0000000..d637f45 --- /dev/null +++ b/tmc2209/address.go @@ -0,0 +1,1409 @@ +package tmc2209 + +import "log" + +// TMC2209 Register addresses +const ( + GCONF = 0x00 + GSTAT = 0x01 + IFCNT = 0x02 + IOIN = 0x06 + IHOLD_IRUN = 0x10 + TPOWERDOWN = 0x11 + TSTEP = 0x12 + TPWMTHRS = 0x13 + TCOOLTHRS = 0x14 + VACTUAL = 0x22 + SGTHRS = 0x40 + SG_RESULT = 0x41 + COOLCONF = 0x42 + MSCNT = 0x6A + MSCURACT = 0x6B + CHOPCONF = 0x6C + DRV_STATUS = 0x6F + PWMCONF = 0x70 + PWM_SCALE = 0x71 + PWM_AUTO = 0x72 + expectedVersion = 0x03 +) + +type RegisterComm interface { + ReadRegister(register uint8, driverIndex uint8) (uint32, error) + WriteRegister(register uint8, value uint32, driverIndex uint8) error +} + +// Register is an interface that all register structs will implement for generic access +type Register interface { + Pack() uint32 + Unpack(value uint32) + GetAddress() uint8 +} + +// NewRegister is a generic function to initialize any register with a given address +func NewRegister(registerAddr uint8) Register { + switch registerAddr { + case IOIN: + return NewIoin() + case PWMCONF: + return NewPWMConf() + case GCONF: + return NewGconf() + case GSTAT: + return NewGstat() + case IFCNT: + return NewIfcnt() + case IHOLD_IRUN: + return NewIholdIrun() + case TPOWERDOWN: + return NewTpowerdown() + case TSTEP: + return NewTstep() + case TPWMTHRS: + return NewTpwmthrs() + case TCOOLTHRS: + return NewTcoolthrs() + case VACTUAL: + return NewVactual() + case SGTHRS: + return NewSgthrs() + case SG_RESULT: + return NewSgResult() + case COOLCONF: + return NewCoolConf() + case MSCNT: + return NewMscnt() + case MSCURACT: + return NewMscuract() + case CHOPCONF: + return NewChopconf() + case DRV_STATUS: + return NewDrvStatus() + case PWM_SCALE: + return NewPwmScale() + case PWM_AUTO: + return NewPwmAuto() + default: + return nil + } +} + +// ReadRegister function using the register constants +func ReadRegister(comm RegisterComm, driverIndex uint8, register uint8) (uint32, error) { + // Read the register value using the comm interface + + value, err := comm.ReadRegister(register, driverIndex) + log.Printf("Request read ", register, driverIndex, value) + if err != nil { + return 0, err + } + return value, nil +} + +// WriteRegister function using the register constants +func WriteRegister(comm RegisterComm, register uint8, driverIndex uint8, value uint32) error { + // Write the value to the register using the comm interface + return comm.WriteRegister(register, value, driverIndex) +} + +// Ioin represents the fields of the IOIN register (0x06) in the TMC2209 +// +// The IOIN register provides access to various inputs and control signals +// that the driver uses to determine its current state. This register contains +// bits related to motor driver status, step input, direction, diagnostics, +// and other control signals. +// +// The structure represents each field in the IOIN register and provides +// methods to pack and unpack these fields into the `Bytes` field (a 32-bit +// packed representation of all fields) for easier access and manipulation. +// +// Fields: +// - Enn: Enables the driver. 1 = Driver enabled, 0 = Driver disabled. +// - Reserved0: Reserved bit, must be set to 0. +// - Ms1: Microstep setting, first bit of the microstep configuration. +// - Ms2: Microstep setting, second bit of the microstep configuration. +// - Diag: Diagnostics flag. Used for fault detection and error reporting. +// - Reserved1: Reserved bit, must be set to 0. +// - PdnSerial: Power-down (sleep) state for the UART interface. 1 = Power down, 0 = Active. +// - Step: Step input signal. 1 = Step signal active, 0 = No step signal. +// - SpreadEn: SpreadCycle enable. 1 = Enable SpreadCycle, 0 = Disable SpreadCycle. +// - Dir: Direction input. 1 = Reverse direction, 0 = Forward direction. +// - Reserved2: Reserved bits, must be set to 0. +// - Version: Driver version information. The version of the IOIN register in the TMC2209 chip. +// +// `Bytes`: A 32-bit value that stores the packed representation of the IOIN register. +// The `Bytes` field is used to manipulate the register's value as a single 32-bit value, +// allowing you to read and write it more efficiently. +type Ioin struct { + Enn uint32 // 1-bit field: Driver enable status (1 = enabled, 0 = disabled) + Reserved0 uint32 // 1-bit field: Reserved, should always be 0 + Ms1 uint32 // 1-bit field: Microstep setting (first bit) + Ms2 uint32 // 1-bit field: Microstep setting (second bit) + Diag uint32 // 1-bit field: Diagnostics flag (error reporting) + Reserved1 uint32 // 1-bit field: Reserved, should always be 0 + PdnSerial uint32 // 1-bit field: Power-down state for the UART interface (1 = power down, 0 = active) + Step uint32 // 1-bit field: Step signal input (1 = active, 0 = inactive) + SpreadEn uint32 // 1-bit field: SpreadCycle enable (1 = enabled, 0 = disabled) + Dir uint32 // 1-bit field: Direction input (1 = reverse, 0 = forward) + Reserved2 uint32 // 14-bit field: Reserved bits, should always be 0 + Version uint32 // 8-bit field: Version information for the driver + Bytes uint32 // 32-bit field: Packed representation of the IOIN register (all fields packed into a single 32-bit value) + RegisterAddr uint8 // The address of the register, in this case, IOIN (0x06) +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +// This method combines all the individual fields (like Enn, Ms1, etc.) +// into a packed 32-bit value that can be written to the register. +func (ioin *Ioin) Pack() uint32 { + ioin.Bytes = (ioin.Enn & 0x01) | // Enn field (1 bit) + ((ioin.Reserved0 & 0x01) << 1) | // Reserved0 field (1 bit) + ((ioin.Ms1 & 0x01) << 2) | // Ms1 field (1 bit) + ((ioin.Ms2 & 0x01) << 3) | // Ms2 field (1 bit) + ((ioin.Diag & 0x01) << 4) | // Diag field (1 bit) + ((ioin.Reserved1 & 0x01) << 5) | // Reserved1 field (1 bit) + ((ioin.PdnSerial & 0x01) << 6) | // PdnSerial field (1 bit) + ((ioin.Step & 0x01) << 7) | // Step field (1 bit) + ((ioin.SpreadEn & 0x01) << 8) | // SpreadEn field (1 bit) + ((ioin.Dir & 0x01) << 9) | // Dir field (1 bit) + ((ioin.Reserved2 & 0x3FFF) << 10) | // Reserved2 field (14 bits) + ((ioin.Version & 0xFF) << 24) // Version field (8 bits) + return ioin.Bytes +} + +// Unpack the Bytes field into the individual fields. +// This method takes the packed 32-bit value from the Bytes field and extracts +// the individual register fields into their corresponding variables. +func (ioin *Ioin) Unpack(uint32) { + ioin.Enn = ioin.Bytes & 0x01 + ioin.Reserved0 = (ioin.Bytes >> 1) & 0x01 + ioin.Ms1 = (ioin.Bytes >> 2) & 0x01 + ioin.Ms2 = (ioin.Bytes >> 3) & 0x01 + ioin.Diag = (ioin.Bytes >> 4) & 0x01 + ioin.Reserved1 = (ioin.Bytes >> 5) & 0x01 + ioin.PdnSerial = (ioin.Bytes >> 6) & 0x01 + ioin.Step = (ioin.Bytes >> 7) & 0x01 + ioin.SpreadEn = (ioin.Bytes >> 8) & 0x01 + ioin.Dir = (ioin.Bytes >> 9) & 0x01 + ioin.Reserved2 = (ioin.Bytes >> 10) & 0x3FFF + ioin.Version = (ioin.Bytes >> 24) & 0xFF +} +func (ioin *Ioin) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, ioin.RegisterAddr) +} +func (ioin *Ioin) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, ioin.RegisterAddr, driverIndex, value) +} +func (ioin *Ioin) GetAddress() uint8 { + return ioin.RegisterAddr +} +func NewIoin() *Ioin { + return &Ioin{ + RegisterAddr: IOIN, + } +} + +// PWMConf represents the fields in the TMC2209 PWMCONF register. +// +// This register controls the Pulse Width Modulation (PWM) configuration for the stepper motor. +// It defines parameters related to the motor's driving mechanism, including PWM offset, +// gradient, frequency, and scaling. These parameters allow fine control over the motor's +// behavior, particularly in the context of energy saving modes like StealthChop. +// +// The fields are as follows: +// - **PwmOfs** (8 bits): The offset for PWM, affecting the motor's drive current. +// - **PwmGrad** (8 bits): The gradient applied to the PWM waveform. It determines the +// rise and fall of the PWM signal, influencing motor smoothness and efficiency. +// - **PwmFreq** (2 bits): PWM frequency control, affecting the switching rate of the motor. +// This value influences the motor's operating frequency and noise characteristics. +// - **PwmAutoscale** (1 bit): This flag enables automatic scaling of the PWM amplitude +// based on the motor’s load. When enabled, the motor adjusts its drive based on current +// requirements, improving energy efficiency and reducing heat generation. +// - **PwmAutograd** (1 bit): Similar to `PwmAutoscale`, but focuses on the gradient +// of the PWM signal, adjusting for the motor's behavior dynamically to maintain efficient +// operation under varying load conditions. +// - **Freewheel** (2 bits): Determines the state of the motor's freewheel functionality, +// which allows the motor to coast when not actively driven, thereby reducing power consumption. +// - **PwmReg** (4 bits): Register for fine-tuning the PWM signal’s behavior. It is typically used +// for customizing the PWM waveform for optimal performance in different conditions. +// - **PwmLim** (4 bits): PWM limit for the motor’s current control. It provides an upper +// bound for the current drive to prevent overheating or excessive current draw. +// +// The `PWMCONF` register allows for precise control over the motor's electrical characteristics, +// contributing to better performance and energy efficiency, particularly in StealthChop mode. +type PWMConf struct { + PwmOfs uint32 // 8 bits + PwmGrad uint32 // 8 bits + PwmFreq uint32 // 2 bits + PwmAutoscale uint32 // 1 bit + PwmAutograd uint32 // 1 bit + Freewheel uint32 // 2 bits + PwmReg uint32 // 4 bits + PwmLim uint32 // 4 bits + Bytes uint32 // 32-bit packed representation of all fields + RegisterAddr uint8 +} + +func (pwm *PWMConf) GetAddress() uint8 { + return pwm.RegisterAddr +} + +// NewPWMConf Initialize PWMConf with register address +func NewPWMConf() *PWMConf { + return &PWMConf{ + RegisterAddr: PWMCONF, + } +} + +// Pack Method to pack the fields into the Bytes field +func (pwm *PWMConf) Pack() uint32 { + // Pack the individual fields into the Bytes field + pwm.Bytes = (pwm.PwmOfs & 0xFF) | + ((pwm.PwmGrad & 0xFF) << 8) | + ((pwm.PwmFreq & 0x03) << 16) | + ((pwm.PwmAutoscale & 0x01) << 18) | + ((pwm.PwmAutograd & 0x01) << 19) | + ((pwm.Freewheel & 0x03) << 20) | + ((pwm.PwmReg & 0x0F) << 24) | + ((pwm.PwmLim & 0x0F) << 28) // PWM_LIM is 4 bits and goes in the last 4 bits + return pwm.Bytes +} + +// Unpack Method to unpack the Bytes field into individual fields +func (pwm *PWMConf) Unpack(uint32) { + // Unpack the Bytes field into individual fields + pwm.PwmOfs = pwm.Bytes & 0xFF + pwm.PwmGrad = (pwm.Bytes >> 8) & 0xFF + pwm.PwmFreq = (pwm.Bytes >> 16) & 0x03 + pwm.PwmAutoscale = (pwm.Bytes >> 18) & 0x01 + pwm.PwmAutograd = (pwm.Bytes >> 19) & 0x01 + pwm.Freewheel = (pwm.Bytes >> 20) & 0x03 + pwm.PwmReg = (pwm.Bytes >> 24) & 0x0F + pwm.PwmLim = (pwm.Bytes >> 28) & 0x0F +} +func (pwm *PWMConf) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, pwm.RegisterAddr) +} +func (pwm *PWMConf) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, driverIndex, pwm.RegisterAddr, value) +} + +// Chopconf represents the fields in the TMC2209 CHOPCONF register. +// +// The CHOPCONF register configures the chopping control for the stepper motor. +// It defines parameters related to the current waveform, step behavior, and +// microstepping resolution. These parameters directly affect the motor's efficiency, +// torque output, and smoothness, especially when operating in StealthChop mode. +// +// The fields are as follows: +// - **Toff** (4 bits): The Off-time parameter for the chopping waveform. +// It sets the duration of the 'off' period of the waveform, which controls how long +// the motor driver remains idle between each pulse. Adjusting this value affects motor efficiency +// and noise characteristics. Lower values allow faster switching and more responsive motors, +// while higher values can improve efficiency at the cost of motor speed. +// - **Hstrt** (3 bits): The Hysteresis start value. This parameter controls the start +// of the hysteresis in the chopping process. It determines when the current will be limited +// based on the hysteresis function. The higher the value, the more gradual the transition +// between phases. +// - **Hend** (4 bits): The Hysteresis end value. This value defines the end of the hysteresis +// region, where the current is stabilized. Like `Hstrt`, higher values result in a smoother +// transition in current regulation and lower torque ripple. +// - **Tbl** (2 bits): The Table selection for the internal chopping table, affecting +// the switching behavior and optimization of the motor’s current profile. +// This setting is typically tuned for different load conditions or step modes. +// - **Vsense** (1 bit): This field selects the sense resistor mode. If enabled, the driver +// uses the internal sense resistors for current sensing, which enables more precise current +// regulation and monitoring, enhancing the motor’s performance and efficiency. +// - **Mres** (4 bits): The microstepping resolution. This value controls how many steps the motor +// takes per full rotation. Higher values allow for finer stepping, leading to smoother motion +// but at the cost of reduced torque at higher microstep resolutions. +// - **Intpol** (1 bit): This field enables interpolation in the motor driver, which improves +// smoothness by interpolating intermediate microsteps between full steps. It helps reduce +// torque ripple and improve overall motor performance. +// - **Dedge** (1 bit): This parameter enables the detection of a specific edge (rising or falling) +// in the step pulse, which is used to control the timing of motor transitions. +// It is generally used for fine-tuning performance or reducing noise during operation. +// - **Diss2g** (1 bit): This field, when enabled, disables the second MOSFET during idle phases +// in the motor operation, further improving efficiency and reducing heat generation. +// - **Diss2vs** (1 bit): Similar to `Diss2g`, this parameter disables the second MOSFET during +// specific phases of operation to reduce losses, increase efficiency, and improve motor control +// in certain scenarios, particularly during low-speed or low-power operation. +// +// The `CHOPCONF` register plays a crucial role in fine-tuning motor operation, ensuring +// smooth motion, efficient power consumption, and minimizing torque ripple in the system. +// These settings are particularly useful when transitioning between operating modes like StealthChop. +type Chopconf struct { + Toff uint32 + Hstrt uint32 + Hend uint32 + Tbl uint32 + Vsense uint32 + Mres uint32 + Intpol uint32 + Dedge uint32 + Diss2g uint32 + Diss2vs uint32 + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 +} + +func (chopconf *Chopconf) GetAddress() uint8 { + return chopconf.RegisterAddr +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (chopconf *Chopconf) Pack() uint32 { + chopconf.Bytes = (chopconf.Toff & 0x0F) | + ((chopconf.Hstrt & 0x07) << 4) | + ((chopconf.Hend & 0x0F) << 7) | + ((chopconf.Tbl & 0x03) << 15) | + ((chopconf.Vsense & 0x01) << 17) | + ((chopconf.Mres & 0x0F) << 24) | + ((chopconf.Intpol & 0x01) << 28) | + ((chopconf.Dedge & 0x01) << 29) | + ((chopconf.Diss2g & 0x01) << 30) | + ((chopconf.Diss2vs & 0x01) << 31) + return chopconf.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (chopconf *Chopconf) Unpack(uint32) { + chopconf.Toff = chopconf.Bytes & 0x0F + chopconf.Hstrt = (chopconf.Bytes >> 4) & 0x07 + chopconf.Hend = (chopconf.Bytes >> 7) & 0x0F + chopconf.Tbl = (chopconf.Bytes >> 15) & 0x03 + chopconf.Vsense = (chopconf.Bytes >> 17) & 0x01 + chopconf.Mres = (chopconf.Bytes >> 24) & 0x0F + chopconf.Intpol = (chopconf.Bytes >> 28) & 0x01 + chopconf.Dedge = (chopconf.Bytes >> 29) & 0x01 + chopconf.Diss2g = (chopconf.Bytes >> 30) & 0x01 + chopconf.Diss2vs = (chopconf.Bytes >> 31) & 0x01 +} +func NewChopconf() *Chopconf { + return &Chopconf{ + RegisterAddr: CHOPCONF, + } +} +func (chopconf *Chopconf) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, chopconf.RegisterAddr) +} +func (chopconf *Chopconf) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, chopconf.RegisterAddr, driverIndex, value) +} + +// Gstat represents the fields in the TMC2209 GSTAT register. +type Gstat struct { + Reset uint32 + DrvErr uint32 + UvCp uint32 + Reserved uint32 + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 +} + +func (gstat *Gstat) GetAddress() uint8 { + return gstat.RegisterAddr +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (gstat *Gstat) Pack() uint32 { + gstat.Bytes = (gstat.Reset & 0x01) | + ((gstat.DrvErr & 0x01) << 1) | + ((gstat.UvCp & 0x01) << 2) | + ((gstat.Reserved & 0x1FFFFF) << 3) // 21 bits reserved + return gstat.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (gstat *Gstat) Unpack(uint32) { + gstat.Reset = gstat.Bytes & 0x01 + gstat.DrvErr = (gstat.Bytes >> 1) & 0x01 + gstat.UvCp = (gstat.Bytes >> 2) & 0x01 + gstat.Reserved = (gstat.Bytes >> 3) & 0x1FFFFF +} +func NewGstat() *Gstat { + return &Gstat{ + RegisterAddr: GSTAT, + } +} +func (gstat *Gstat) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, gstat.RegisterAddr) +} +func (gstat *Gstat) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, gstat.RegisterAddr, driverIndex, value) +} + +// Gconf represents the fields in the TMC2209 GCONF register. +// +// The GCONF register configures global settings for the motor driver. +// It controls various aspects of the operation, including stepper driver modes, +// fault detection, and other motor control behaviors that influence motor performance, +// efficiency, and system diagnostics. The register also enables or disables certain features +// based on specific application requirements. +// +// The fields are as follows: +// - **IScaleAnalog** (1 bit): This field controls the scaling of the analog current sense. +// If enabled, it adjusts the scaling of the motor current detection circuitry, improving +// the precision of current regulation. This is important for accurate motor control and +// better torque handling, especially at low speeds. +// - **InternalRsense** (1 bit): When enabled, this field indicates that the internal sense +// resistors are used for current sensing. This allows for improved accuracy in current +// measurement and feedback, resulting in better overall motor performance and energy efficiency. +// - **EnSpreadcycle** (1 bit): This field enables SpreadCycle operation mode. When enabled, +// it switches the motor driver to operate in SpreadCycle mode, which is the traditional +// chopper operation mode. This mode typically results in higher efficiency and more precise +// current control, but it may produce more noise compared to StealthChop mode. +// - **Shaft** (1 bit): This field enables the detection of a shaft (step) in the system, which +// can be used to detect whether the motor is moving or has completed a cycle. It is used +// to improve system control and diagnostics in motor-driven applications. +// - **IndexOtpw** (1 bit): This field, when set, allows detection of the over-temperature +// warning flag (otpw), indicating that the motor or the driver has exceeded safe operating +// temperatures. If enabled, the system can take actions like reducing speed or stopping the motor +// to avoid damage due to overheating. +// - **IndexStep** (1 bit): This field enables indexing of the motor steps. When enabled, it +// allows for step detection, which is helpful in stepper motors where exact step counts are critical. +// This setting ensures accurate positioning of the motor in precise step operations. +// - **PdnDisable** (1 bit): This field enables or disables the power-down functionality for +// the driver. When enabled, the driver will power down the motor and enter a low-power state. +// This is typically used to reduce energy consumption when the motor is not active or when the +// system is in idle mode. This helps save power during periods of non-operation. +// - **MstepRegSelect** (1 bit): This field allows the selection of the multistep regulation +// mode. When enabled, the driver uses the multistep algorithm to control current regulation, +// which can help reduce torque ripple and improve motor smoothness, particularly in applications +// requiring high precision or smooth motion. +// - **MultistepFilt** (1 bit): When enabled, this field activates the multistep filtering. +// This feature is used to reduce the effect of noise and fluctuations in the current waveform. +// It smooths out the power supply and motor performance, particularly at higher speeds or during +// high-load conditions. +// - **Reserved** (21 bits): These reserved bits are unused and should always be set to zero. +// They ensure backward compatibility and alignment with future versions of the register. +// They have no effect on the system's operation. +type Gconf struct { + IScaleAnalog uint32 + InternalRsense uint32 + EnSpreadcycle uint32 + Shaft uint32 + IndexOtpw uint32 + IndexStep uint32 + PdnDisable uint32 + MstepRegSelect uint32 + MultistepFilt uint32 + Reserved uint32 + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 +} + +func (gconf *Gconf) GetAddress() uint8 { + return gconf.RegisterAddr +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (gconf *Gconf) Pack() uint32 { + gconf.Bytes = (gconf.IScaleAnalog & 0x01) | + ((gconf.InternalRsense & 0x01) << 1) | + ((gconf.EnSpreadcycle & 0x01) << 2) | + ((gconf.Shaft & 0x01) << 3) | + ((gconf.IndexOtpw & 0x01) << 4) | + ((gconf.IndexStep & 0x01) << 5) | + ((gconf.PdnDisable & 0x01) << 6) | + ((gconf.MstepRegSelect & 0x01) << 7) | + ((gconf.MultistepFilt & 0x01) << 8) | + ((gconf.Reserved & 0x1FFFFF) << 9) // 21 bits reserved + return gconf.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (gconf *Gconf) Unpack(uint32) { + gconf.IScaleAnalog = gconf.Bytes & 0x01 + gconf.InternalRsense = (gconf.Bytes >> 1) & 0x01 + gconf.EnSpreadcycle = (gconf.Bytes >> 2) & 0x01 + gconf.Shaft = (gconf.Bytes >> 3) & 0x01 + gconf.IndexOtpw = (gconf.Bytes >> 4) & 0x01 + gconf.IndexStep = (gconf.Bytes >> 5) & 0x01 + gconf.PdnDisable = (gconf.Bytes >> 6) & 0x01 + gconf.MstepRegSelect = (gconf.Bytes >> 7) & 0x01 + gconf.MultistepFilt = (gconf.Bytes >> 8) & 0x01 + gconf.Reserved = (gconf.Bytes >> 9) & 0x1FFFFF +} +func NewGconf() *Gconf { + return &Gconf{ + RegisterAddr: GCONF, + } +} +func (gconf *Gconf) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, gconf.RegisterAddr, driverIndex) +} +func (gconf *Gconf) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, gconf.RegisterAddr, driverIndex, value) +} + +// Ifcnt represents the fields in the TMC2209 IFCNT register. +// +// The IFCNT register is used to monitor the input frequency of the step signal. It holds the +// count of the number of steps that have been processed by the driver and provides +// valuable information about the frequency of step pulses being received. This can be useful +// for diagnostic purposes, to measure the actual step rate, or to check the system’s input +// signal behavior. It helps in monitoring the stepper signal's integrity and rate of pulses +// generated by the control system. +// +// The field is as follows: +// - **Ifcnt** (8 bits): This 8-bit field holds the count of the received step pulses. The count +// is incremented on each step pulse the driver receives and represents the number of steps +// processed by the motor driver. The counter resets at the start of each pulse cycle. This +// field can be used to verify the input signal's frequency and detect potential issues +// such as missed or delayed pulses. Monitoring this register can help optimize the stepper +// signal for better accuracy and performance. +type Ifcnt struct { + Ifcnt uint32 // 8-bit interface counter + Reserved uint32 // Reserved bits, here represented as uint32 for simplicity + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 +} + +func (ifcnt *Ifcnt) GetAddress() uint8 { + return ifcnt.RegisterAddr +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (ifcnt *Ifcnt) Pack() uint32 { + ifcnt.Bytes = (ifcnt.Ifcnt & 0xFF) | // 8 bits for the interface counter + ((ifcnt.Reserved & 0xFFFFFF) << 8) // Remaining bits for reserved (24 bits) + return ifcnt.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (ifcnt *Ifcnt) Unpack(uint32) { + ifcnt.Ifcnt = ifcnt.Bytes & 0xFF + ifcnt.Reserved = (ifcnt.Bytes >> 8) & 0xFFFFFF +} + +// Initialize IFCNT with register address +func NewIfcnt() *Ifcnt { + return &Ifcnt{ + RegisterAddr: IFCNT, // IFCNT register address + } +} +func (ifcnt *Ifcnt) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, ifcnt.RegisterAddr) +} +func (ifcnt *Ifcnt) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, ifcnt.RegisterAddr, driverIndex, value) +} + +// IholdIrun represents the fields in the TMC2209 IHOLD_IRUN register. +// +// The IHOLD_IRUN register is used to configure the current settings for the motor during +// different phases of operation. It specifically defines two current settings: the hold current +// and the run current. +// +// - **Ihold** (5 bits): This field defines the current value for holding the motor in place when +// it is idle. The value can be set between 0 and 31, with 0 representing the lowest possible current, +// and 31 representing the maximum hold current. The hold current is typically lower than the run current +// to minimize energy consumption when the motor is not actively moving. +// +// - **Iruns** (5 bits): This field sets the current for the motor when it is running (i.e., moving). +// The value can range from 0 to 31, and higher values represent stronger currents for increased torque +// when the motor is under load. The run current is used when the motor is actively engaged in motion. +// +// - **Iholddelay** (4 bits): This field defines the delay in microsteps before transitioning between +// the hold current and the run current. This delay allows for smoother transitions when starting or stopping +// the motor, preventing sudden jumps in current that could cause issues with the stepper system. +// +// This register is critical for efficient motor control, allowing dynamic management of motor currents to optimize +// energy consumption and torque output. Tuning the `Ihold` and `Iruns` settings can significantly impact motor +// performance and efficiency, especially in applications where energy efficiency is important. +type IholdIrun struct { + Ihold uint32 // 5 bits for hold current + Irun uint32 // 5 bits for run current + Iholddelay uint32 // 4 bits for hold delay + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 // Register address +} + +func (iholdIrun *IholdIrun) GetAddress() uint8 { + return iholdIrun.RegisterAddr +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (iholdIrun *IholdIrun) Pack() uint32 { + iholdIrun.Bytes = (iholdIrun.Ihold & 0x1F) | // 5 bits for IHOLD + ((iholdIrun.Irun & 0x1F) << 5) | // 5 bits for IRUN + ((iholdIrun.Iholddelay & 0x0F) << 10) // 4 bits for IHOLDD_DELAY + return iholdIrun.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (iholdIrun *IholdIrun) Unpack(uint32) { + iholdIrun.Ihold = iholdIrun.Bytes & 0x1F + iholdIrun.Irun = (iholdIrun.Bytes >> 5) & 0x1F + iholdIrun.Iholddelay = (iholdIrun.Bytes >> 10) & 0x0F +} + +// Initialize IHOLD_IRUN with register address +func NewIholdIrun() *IholdIrun { + return &IholdIrun{ + RegisterAddr: IHOLD_IRUN, // IHOLD_IRUN register address + } +} +func (iholdIrun *IholdIrun) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, iholdIrun.RegisterAddr) +} +func (iholdIrun *IholdIrun) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, iholdIrun.RegisterAddr, driverIndex, value) +} + +// Tpwmthrs represents the fields in the TMC2209 TPWMTHRS register. +// +// The TPWMTHRS register is used to set the threshold for transitioning between +// traditional stepper driving (full stepping) and the use of PWM (Pulse Width Modulation) +// for controlling the stepper motor. This register allows for better current control +// and smoother operation, especially at higher speeds. +// +// - **Tpwmthrs** (20 bits): This field defines the threshold value for switching +// from traditional stepper driving to PWM control. The motor will operate with +// standard stepping until the velocity exceeds the value set in this register, +// at which point it will switch to PWM mode. This allows for improved efficiency +// and smoother operation at higher speeds. +// +// The value for the `Tpwmthrs` field is typically set based on the specific motor and +// the desired speed range for the application. A higher threshold means the motor +// will operate in traditional stepping mode at lower speeds, while a lower threshold +// will enable PWM control at lower speeds for better motor control and efficiency. +type Tpwmthrs struct { + Threshold uint32 // 32-bit threshold value + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 // Register address +} + +func (tpwmthrs *Tpwmthrs) GetAddress() uint8 { + return tpwmthrs.RegisterAddr +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (tpwmthrs *Tpwmthrs) Pack() uint32 { + tpwmthrs.Bytes = tpwmthrs.Threshold & 0xFFFFFFFF // 32-bit threshold value + return tpwmthrs.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (tpwmthrs *Tpwmthrs) Unpack(uint32) { + tpwmthrs.Threshold = tpwmthrs.Bytes & 0xFFFFFFFF +} + +// NewTpwmthrs Initialize TPWMTHRS with register address +func NewTpwmthrs() *Tpwmthrs { + return &Tpwmthrs{ + RegisterAddr: TPWMTHRS, // TPWMTHRS register address + } +} +func (tpwmthrs *Tpwmthrs) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, tpwmthrs.RegisterAddr) +} +func (tpwmthrs *Tpwmthrs) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, tpwmthrs.RegisterAddr, driverIndex, value) +} + +// Vactual represents the fields in the TMC2209 VACTUAL register. +// +// The VACTUAL register provides the actual velocity of the motor in terms of the +// stepper's step clock. It is used to monitor the real-time motor speed and is +// an important part of controlling the motor's behavior during operation. +// +// - **Vactual** (20 bits): This field holds the current actual velocity of the motor. +// It represents the stepper's velocity in terms of the step clock, providing feedback +// on how fast the motor is turning. The value in this register is updated regularly +// based on the microstepping and the actual motor speed. +// +// The `VACTUAL` register is particularly useful for closed-loop control systems, +// where the actual motor speed needs to be compared against the desired speed +// (set in other registers) to make adjustments. This can help in fine-tuning the +// motor's behavior for smoother operation and more accurate performance. +type Vactual struct { + Velocity uint32 // 32-bit velocity value + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 // Register address +} + +func (vactual *Vactual) GetAddress() uint8 { + return vactual.RegisterAddr +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (vactual *Vactual) Pack() uint32 { + vactual.Bytes = vactual.Velocity & 0xFFFFFFFF // 32-bit velocity value + return vactual.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (vactual *Vactual) Unpack(uint32) { + vactual.Velocity = vactual.Bytes & 0xFFFFFFFF +} + +// Initialize VACTUAL with register address +func NewVactual() *Vactual { + return &Vactual{ + RegisterAddr: VACTUAL, // VACTUAL register address + } +} +func (vactual *Vactual) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, vactual.RegisterAddr) +} +func (vactual *Vactual) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, vactual.RegisterAddr, driverIndex, value) +} + +// Tcoolthrs represents the fields in the TMC2209 TCOOLTHRS register. +// +// The TCOOLTHRS register sets the lower threshold velocity for switching on +// smart energy CoolStep and StallGuard output to the DIAG pin. This helps +// manage the motor's energy usage by enabling CoolStep at higher velocities +// and StallGuard at lower velocities. +// +// - **TCOOLTHRS** (20 bits): The threshold velocity (in units of steps per +// microstep) used to switch between CoolStep and StallGuard modes. The motor +// operates in CoolStep mode if the velocity exceeds this threshold and uses +// StallGuard if the velocity falls below it. +// +// This register is important for optimizing motor performance, especially +// in applications that require energy efficiency. It ensures that the motor +// does not use unnecessary energy at low speeds while still being able to detect stalls +// and adjust torque at higher speeds. CoolStep allows the driver to dynamically adjust +// current levels based on the motor's actual load, reducing power consumption. +type Tcoolthrs struct { + Velocity uint32 // 20 bits for velocity + Bytes uint32 // Packed 32-bit value + RegisterAddr uint8 +} + +func (tcoolthrs *Tcoolthrs) GetAddress() uint8 { + return tcoolthrs.RegisterAddr +} + +// Initialize TCOOLTHRS with register address +func NewTcoolthrs() *Tcoolthrs { + return &Tcoolthrs{ + RegisterAddr: TCOOLTHRS, + } +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (tcoolthrs *Tcoolthrs) Pack() uint32 { + tcoolthrs.Bytes = tcoolthrs.Velocity & 0xFFFFF // Keep only the lower 20 bits + return tcoolthrs.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (tcoolthrs *Tcoolthrs) Unpack(uint32) { + tcoolthrs.Velocity = tcoolthrs.Bytes & 0xFFFFF +} +func (tcoolthrs *Tcoolthrs) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, tcoolthrs.RegisterAddr) +} +func (tcoolthrs *Tcoolthrs) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, tcoolthrs.RegisterAddr, driverIndex, value) +} + +// Sgthrs represents the fields in the TMC2209 SGTHRS register. +// +// The SGTHRS register sets the detection threshold for stall detection in the motor. +// It compares the StallGuard result (SG_RESULT) to twice the value set in this register. +// If the SG_RESULT value falls below the threshold (SG_RESULT < SGTHRS * 2), a stall +// is detected and can trigger a response such as a warning or motor shutdown. +// +// - **SGTHRS** (8 bits): This value sets the detection threshold for the StallGuard feature. +// The result of the StallGuard measurement (SG_RESULT) is compared to this threshold value +// multiplied by 2. When the SG_RESULT value is less than this threshold, a stall is detected. +// +// The `SGTHRS` register is part of the StallGuard feature, which allows the driver to +// detect and respond to motor stalls. StallGuard provides real-time feedback on the motor's +// status by measuring the motor’s back EMF to detect any irregularities that might indicate +// a stall condition. This can help in preventing mechanical damage by responding to stalls early. +type Sgthrs struct { + Threshold uint32 // 8 bits for threshold value + Bytes uint32 // Packed 32-bit value + RegisterAddr uint8 +} + +func (sgthrs *Sgthrs) GetAddress() uint8 { + return sgthrs.RegisterAddr +} + +// Initialize SGTHRS with register address +func NewSgthrs() *Sgthrs { + return &Sgthrs{ + RegisterAddr: SGTHRS, + } +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (sgthrs *Sgthrs) Pack() uint32 { + sgthrs.Bytes = sgthrs.Threshold & 0xFF // Keep only the lower 8 bits + return sgthrs.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (sgthrs *Sgthrs) Unpack(uint32) { + sgthrs.Threshold = sgthrs.Bytes & 0xFF +} +func (sgthrs *Sgthrs) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, sgthrs.RegisterAddr) +} +func (sgthrs *Sgthrs) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, sgthrs.RegisterAddr, driverIndex, value) +} + +// SgResult represents the fields in the TMC2209 SG_RESULT register. +// +// The SG_RESULT register stores the result of the StallGuard measurement, which is used +// to detect motor stalls. The StallGuard feature measures the motor's back electromotive force +// (back EMF) to assess the motor's load and detect irregularities that could indicate a stall. +// +// - **SG_RESULT** (10 bits): The register holds the result of the StallGuard measurement, +// which provides information about the motor's load and stall condition. A higher value in +// SG_RESULT generally means the motor is operating normally, while a lower value indicates +// the motor might be experiencing a stall or a mechanical blockage. +// +// The value in the SG_RESULT register is used in conjunction with the `SGTHRS` register +// (StallGuard Threshold) to determine if a stall condition has occurred. If the SG_RESULT +// value is below twice the value set in the `SGTHRS` register, a stall is considered to have occurred. +// +// The SG_RESULT register is particularly useful for motor stall detection, enabling the driver +// to protect the motor from damage caused by excessive load or mechanical binding by halting the motor's operation. +type SgResult struct { + Result uint32 // 10 bits for the result + Bytes uint32 // Packed 32-bit value + RegisterAddr uint8 +} + +func (sgResult *SgResult) GetAddress() uint8 { + return sgResult.RegisterAddr +} + +// NewSgResult Initialize SG_RESULT with register address +func NewSgResult() *SgResult { + return &SgResult{ + RegisterAddr: SG_RESULT, + } +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (sgResult *SgResult) Pack() uint32 { + sgResult.Bytes = sgResult.Result & 0x3FF // Keep only the lower 10 bits + return sgResult.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (sgResult *SgResult) Unpack(uint32) { + sgResult.Result = sgResult.Bytes & 0x3FF +} +func (sgResult *SgResult) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, sgResult.RegisterAddr) +} +func (sgResult *SgResult) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, sgResult.RegisterAddr, driverIndex, value) +} + +// CoolConf represents the fields in the TMC2209 COOLCONF register. +// +// The COOLCONF register controls various settings related to the CoolStep feature, +// which automatically adjusts the motor's current based on the load to optimize power consumption +// and minimize heat generation. +// +// - **SEMIN** (5 bits): The minimum current value for the CoolStep algorithm to be enabled. If the +// motor current falls below this value, CoolStep will reduce the current. The SEMIN field helps to +// control the minimum threshold for current scaling. +// +// - **SEUP** (2 bits): The step-up value for the current when CoolStep detects an increase in load. +// It defines the amount by which the motor current is increased when the load increases and the motor +// is at risk of stalling. It helps to balance current efficiency and motor performance. +// +// - **SEMAX** (5 bits): The maximum current value for CoolStep. This value sets the upper threshold +// for the current when the motor is under heavy load. It ensures that the motor can handle the load +// by increasing the current when necessary, while still maintaining efficiency. +// +// - **SEDN** (2 bits): The step-down value for the current when CoolStep detects a decrease in load. +// It helps to lower the current consumption when the motor is no longer under heavy load, optimizing +// energy efficiency. +// +// - **SEIMIN** (1 bit): Enables or disables the current scaling for low loads. If enabled, the motor +// current will be reduced under low load conditions, improving energy efficiency. If disabled, the +// motor current remains constant at the preset level, regardless of the load. +// +// The COOLCONF register allows for fine-tuning of the motor current scaling behavior based on the load, +// helping to optimize motor efficiency and reduce power consumption and heat generation. +type CoolConf struct { + Semin uint32 // 1 bit + Sedn uint32 // 2 bits (sedn0, sedn1) + Semax uint32 // 4 bits (semax0 to semax3) + Seup uint32 // 3 bits (seup0, seup1, seup2) + Semin2 uint32 // 6 bits (semin0 to semin5) + CoolStepEnable uint32 // 1 bit + Reserved uint32 // Reserved 10 bits + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 // The register address (COOLCONF) +} + +func (coolConf *CoolConf) GetAddress() uint8 { + //TODO implement me + panic("implement me") +} + +// Initialize COOLCONF with register address +func NewCoolConf() *CoolConf { + return &CoolConf{ + RegisterAddr: COOLCONF, + } +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (coolConf *CoolConf) Pack() uint32 { + coolConf.Bytes = (coolConf.Semin & 0x01) | + ((coolConf.Sedn & 0x03) << 1) | + ((coolConf.Semax & 0x0F) << 3) | + ((coolConf.Seup & 0x07) << 7) | + ((coolConf.Semin2 & 0x3F) << 10) | + ((coolConf.CoolStepEnable & 0x01) << 16) | + ((coolConf.Reserved & 0x3FF) << 17) // Reserve 10 bits for reserved fields + return coolConf.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (coolConf *CoolConf) Unpack(uint32) { + coolConf.Semin = coolConf.Bytes & 0x01 + coolConf.Sedn = (coolConf.Bytes >> 1) & 0x03 + coolConf.Semax = (coolConf.Bytes >> 3) & 0x0F + coolConf.Seup = (coolConf.Bytes >> 7) & 0x07 + coolConf.Semin2 = (coolConf.Bytes >> 10) & 0x3F + coolConf.CoolStepEnable = (coolConf.Bytes >> 16) & 0x01 + coolConf.Reserved = (coolConf.Bytes >> 17) & 0x3FF +} +func (coolConf *CoolConf) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, coolConf.RegisterAddr) +} +func (coolConf *CoolConf) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, coolConf.RegisterAddr, driverIndex, value) +} + +// DrvStatus represents the fields in the TMC2209 DRV_STATUS register. +// +// The DRV_STATUS register provides information about the driver’s status, +// including over-temperature warnings, short-circuit detection, and current status. +// +// - **OTPW** (1 bit): Overtemperature warning. If this bit is set to 1, it indicates that the +// temperature of the driver has exceeded the safe operating threshold. This can be used to +// trigger protective actions, such as reducing the current or stopping the motor to prevent damage. +// +// - **OT** (1 bit): Overtemperature fault. If this bit is set to 1, it indicates that the driver +// has overheated and is currently in a fault condition due to excessive temperature. It typically +// results in disabling the motor or reducing motor power until the temperature drops to a safe level. +// +// - **S2GA** (1 bit): Short-to-Ground on A phase (phase A of the motor). This bit is set to 1 +// if a short-circuit is detected between the motor's phase A and ground. This protection feature +// helps to safeguard the driver and motor from short circuits that could lead to damage. +// +// - **S2GB** (1 bit): Short-to-Ground on B phase (phase B of the motor). This bit is set to 1 +// if a short-circuit is detected between the motor's phase B and ground. Similar to the previous +// bit, this provides protection against short-circuits for the B phase. +// +// - **S2VSA** (1 bit): Short-to-VCC on A phase (phase A of the motor). If this bit is set to 1, +// it indicates a short-circuit between phase A of the motor and the power supply voltage (VCC). +// This condition could be hazardous and requires corrective actions. +// +// - **S2VSB** (1 bit): Short-to-VCC on B phase (phase B of the motor). Similar to S2VSA, this +// bit is set when a short-circuit is detected between phase B of the motor and VCC. +// +// - **OLA** (1 bit): Overcurrent fault on A phase. This bit is set to 1 if the current through +// phase A exceeds the configured threshold, indicating an overcurrent condition. It may indicate +// a problem with the motor or wiring that requires attention. +// +// - **OLB** (1 bit): Overcurrent fault on B phase. Similar to OLA, this bit indicates an overcurrent +// fault in phase B, signaling an issue that needs to be addressed to prevent damage to the system. +// +// - **T120** (1 bit): Timeout fault for phase A (motor A). If this bit is set, it indicates that +// the motor did not receive any signal or is stuck for too long, possibly due to a failure in the +// motor or wiring. It indicates that the system has detected an abnormal motor condition. +// +// - **T143** (1 bit): Timeout fault for phase B (motor B). Similar to T120, this bit indicates +// an issue with phase B, such as the motor not receiving a signal within the expected timeframe. +// +// - **T150** (1 bit): Timeout fault for both phases A and B. This bit is set if both phases are +// experiencing a timeout condition, which could be due to an issue with the motor or control signals. +// +// - **T157** (1 bit): Timeout fault due to thermal shutdown. If this bit is set, it indicates that +// the driver has shut down due to an overtemperature condition, preventing further damage to the system. +// +// - **CS_ACTUAL** (5 bits): This field provides the actual current setting value for the motor. +// It is used to monitor the current being supplied to the motor in real-time, helping to detect any +// discrepancies or performance issues during operation. +// +// - **STEALTH** (1 bit): StealthChop status. When set to 1, this bit indicates that the StealthChop +// mode is active, which is a feature used to reduce motor noise and improve efficiency at low speeds. +// If set to 0, StealthChop is not active, and the driver may be using a more standard operating mode. +// +// - **STST** (1 bit): Step status. If set to 1, this bit indicates that the motor is currently +// moving or stepping. It can be used to detect if the motor is active or idle at any given time. +type DrvStatus struct { + Stst uint32 // Standstill indicator + Stealth uint32 // StealthChop indicator + CsActual uint32 // Actual motor current / smart energy current + T157 uint32 // 157°C comparator + T150 uint32 // 150°C comparator + T143 uint32 // 143°C comparator + T120 uint32 // 120°C comparator + Olb uint32 // Open load indicator phase B + Ola uint32 // Open load indicator phase A + S2vsb uint32 // Low-side short indicator phase B + S2vsa uint32 // Low-side short indicator phase A + S2gb uint32 // Short to ground indicator phase B + S2ga uint32 // Short to ground indicator phase A + Ot uint32 // Overtemperature flag + Otpw uint32 // Overtemperature pre-warning flag + Reserved uint32 // Reserved bits + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 // Register address +} + +func (drvStatus *DrvStatus) GetAddress() uint8 { + return drvStatus.RegisterAddr +} + +// Initialize DRV_STATUS with register address +func NewDrvStatus() *DrvStatus { + return &DrvStatus{ + RegisterAddr: DRV_STATUS, + } +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (drvStatus *DrvStatus) Pack() uint32 { + drvStatus.Bytes = (drvStatus.Stst & 0x01) | + ((drvStatus.Stealth & 0x01) << 1) | + ((drvStatus.CsActual & 0xFFFF) << 2) | // Actual current in bits 16-31 + ((drvStatus.T157 & 0x01) << 18) | + ((drvStatus.T150 & 0x01) << 19) | + ((drvStatus.T143 & 0x01) << 20) | + ((drvStatus.T120 & 0x01) << 21) | + ((drvStatus.Olb & 0x01) << 22) | + ((drvStatus.Ola & 0x01) << 23) | + ((drvStatus.S2vsb & 0x01) << 24) | + ((drvStatus.S2vsa & 0x01) << 25) | + ((drvStatus.S2gb & 0x01) << 26) | + ((drvStatus.S2ga & 0x01) << 27) | + ((drvStatus.Ot & 0x01) << 28) | + ((drvStatus.Otpw & 0x01) << 29) | + ((drvStatus.Reserved & 0x7FF) << 30) // Reserved bits + return drvStatus.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (drvStatus *DrvStatus) Unpack(uint32) { + drvStatus.Stst = drvStatus.Bytes & 0x01 + drvStatus.Stealth = (drvStatus.Bytes >> 1) & 0x01 + drvStatus.CsActual = (drvStatus.Bytes >> 2) & 0xFFFF + drvStatus.T157 = (drvStatus.Bytes >> 18) & 0x01 + drvStatus.T150 = (drvStatus.Bytes >> 19) & 0x01 + drvStatus.T143 = (drvStatus.Bytes >> 20) & 0x01 + drvStatus.T120 = (drvStatus.Bytes >> 21) & 0x01 + drvStatus.Olb = (drvStatus.Bytes >> 22) & 0x01 + drvStatus.Ola = (drvStatus.Bytes >> 23) & 0x01 + drvStatus.S2vsb = (drvStatus.Bytes >> 24) & 0x01 + drvStatus.S2vsa = (drvStatus.Bytes >> 25) & 0x01 + drvStatus.S2gb = (drvStatus.Bytes >> 26) & 0x01 + drvStatus.S2ga = (drvStatus.Bytes >> 27) & 0x01 + drvStatus.Ot = (drvStatus.Bytes >> 28) & 0x01 + drvStatus.Otpw = (drvStatus.Bytes >> 29) & 0x01 + drvStatus.Reserved = (drvStatus.Bytes >> 30) & 0x7FF +} +func (drvStatus *DrvStatus) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, drvStatus.RegisterAddr) +} +func (drvStatus *DrvStatus) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, drvStatus.RegisterAddr, driverIndex, value) +} + +// PwmScale represents the fields in the TMC2209 PWM_SCALE register. +// +// The PWM_SCALE register provides information related to the current PWM scaling +// values for the motor driver. It is used to determine the effective current +// supplied to the motor and the PWM (Pulse Width Modulation) duty cycle. +// +// - **PwmScaleSum** (8 bits): This field provides the total PWM scaling value used +// for the motor driver. It represents the summed scaling value of both motor phases. +// A higher value typically means more power is delivered to the motor. +// +// - **PwmScaleAuto** (9 bits): This field contains the automatically calculated +// PWM scaling value. It adjusts the power delivery dynamically based on motor +// load and thermal conditions to optimize performance and efficiency. +// The value in this field provides an indication of the current level of scaling +// that the driver is using in the automatic mode, based on the real-time motor conditions. +type PwmScale struct { + PwmScaleSum uint32 // 8-bit PWM duty cycle + PwmScaleAuto int32 // 9-bit signed offset (-255 to +255) + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 // Register address +} + +func (pwm *PwmScale) GetAddress() uint8 { + return pwm.RegisterAddr +} + +// NewPwmScale Initialize PwmScale with register address +func NewPwmScale() *PwmScale { + return &PwmScale{ + RegisterAddr: PWM_SCALE, // PWM_SCALE register address + } +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (pwm *PwmScale) Pack() uint32 { + pwm.Bytes = (pwm.PwmScaleSum & 0xFF) | + ((uint32(pwm.PwmScaleAuto) & 0x1FF) << 8) // 9 bits for PWM_SCALE_AUTO + return pwm.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (pwm *PwmScale) Unpack(uint32) { + pwm.PwmScaleSum = pwm.Bytes & 0xFF + pwm.PwmScaleAuto = int32((pwm.Bytes >> 8) & 0x1FF) // 9-bit signed value +} +func (pwm *PwmScale) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, pwm.RegisterAddr) +} +func (pwm *PwmScale) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, pwm.RegisterAddr, driverIndex, value) +} + +// PwmAuto represents the fields in the TMC2209 PWM_AUTO register. +// +// The PWM_AUTO register is used to configure and monitor the automatic scaling and +// adjustment of PWM settings in the TMC2209 motor driver. This register helps to +// fine-tune motor control behavior based on real-time operating conditions. +// +// - **PwmOfsAuto** (8 bits): This field contains the automatically adjusted PWM +// offset value. The PWM offset is used to set the initial duty cycle for the PWM +// signal. Adjusting this field allows for fine-tuning of motor behavior in +// specific scenarios, such as optimizing torque or minimizing motor heating. +// +// - **PwmGradAuto** (8 bits): This field holds the automatically adjusted PWM +// gradient value. The gradient determines the rate at which the PWM duty cycle +// increases or decreases over time. A higher gradient value can lead to faster +// transitions in power delivery, which is useful for certain motor acceleration +// profiles or applications requiring smooth power changes. +type PwmAuto struct { + PwmOfsAuto int32 // 8-bit signed offset value (-255 to +255) + PwmGradAuto int32 // 8-bit automatically determined gradient value (-255 to +255) + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 // Register address +} + +func (pwm *PwmAuto) GetAddress() uint8 { + return pwm.RegisterAddr +} + +// Initialize PwmAuto with register address +func NewPwmAuto() *PwmAuto { + return &PwmAuto{ + RegisterAddr: PWM_AUTO, // PWM_AUTO register address + } +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (pwm *PwmAuto) Pack() uint32 { + pwm.Bytes = (uint32(pwm.PwmOfsAuto) & 0xFF) | + ((uint32(pwm.PwmGradAuto) & 0xFF) << 8) // 8 bits for each value + return pwm.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (pwm *PwmAuto) Unpack(uint32) { + pwm.PwmOfsAuto = int32(pwm.Bytes & 0xFF) + pwm.PwmGradAuto = int32((pwm.Bytes >> 8) & 0xFF) +} +func (pwm *PwmAuto) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, pwm.RegisterAddr) +} +func (pwm *PwmAuto) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, pwm.RegisterAddr, driverIndex, value) +} + +// Tpowerdown represents the fields in the TMC2209 TPOWERDOWN register. +// +// The TPOWERDOWN register is used to configure the time delay before the driver +// enters the power-down state after the motor has been idle. This is useful for +// energy-saving purposes and to reduce the overall power consumption when the +// motor is not being actively driven. +// +// - **Tpowerdown** (8 bits): This field specifies the time delay (in microseconds) +// before the TMC2209 enters the power-down mode. The power-down mode reduces the +// current drawn by the motor and motor driver, which helps save energy when the +// motor is not in use. The delay is adjustable to balance between responsiveness +// and power consumption. +type Tpowerdown struct { + DelayTime uint32 // Delay time from standstill detection to motor current power-down (8 bits) + RegisterAddr uint8 // Register address + Bytes uint32 // The packed 32-bit value +} + +func (tpd *Tpowerdown) GetAddress() uint8 { + return tpd.RegisterAddr +} + +// Initialize Tpowerdown with register address +func NewTpowerdown() *Tpowerdown { + return &Tpowerdown{ + RegisterAddr: TPOWERDOWN, // TPOWERDOWN register address + } +} + +// Pack the DelayTime field into the Bytes field (a single 8-bit value). +func (tpd *Tpowerdown) Pack() uint32 { + tpd.Bytes = tpd.DelayTime & 0xFF + return tpd.Bytes +} + +// Unpack the Bytes field into the DelayTime field. +func (tpd *Tpowerdown) Unpack(uint32) { + tpd.DelayTime = tpd.Bytes & 0xFF +} +func (tpd *Tpowerdown) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, tpd.RegisterAddr) +} +func (tpd *Tpowerdown) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, tpd.RegisterAddr, driverIndex, value) +} + +// Tstep represents the fields in the TMC2209 TSTEP register. +// +// The TSTEP register configures the inter-step duration, which is the time between +// the steps sent to the motor driver. This is important for controlling the speed +// of the motor, and is typically used to set the stepping frequency. +// +// - **Tstep** (24 bits): This field specifies the duration of time between each step +// signal, measured in microseconds. The value determines how quickly the motor steps, +// effectively controlling the motor's speed. Smaller values result in faster stepping +// (higher speed), while larger values reduce the stepping frequency (lower speed). +// +// The `TSTEP` register is critical for applications that require precise motor control, +// especially for variable-speed applications. The duration set in `TSTEP` should be +// chosen based on the desired motor speed and the capabilities of the motor driver. +type Tstep struct { + StepTime uint32 // Time between 1/256 microsteps (20 bits) + RegisterAddr uint8 // Register address + Bytes uint32 // The packed 32-bit value +} + +func (tstep *Tstep) GetAddress() uint8 { + return tstep.RegisterAddr +} + +// Initialize Tstep with register address +func NewTstep() *Tstep { + return &Tstep{ + RegisterAddr: TSTEP, // TSTEP register address + } +} + +// Pack the StepTime field into the Bytes field (a single 20-bit value). +func (tstep *Tstep) Pack() uint32 { + tstep.Bytes = tstep.StepTime & 0xFFFFF // 20 bits for TSTEP + return tstep.Bytes +} + +// Unpack the Bytes field into the StepTime field. +func (tstep *Tstep) Unpack(uint32) { + tstep.StepTime = tstep.Bytes & 0xFFFFF +} +func (tstep *Tstep) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, tstep.RegisterAddr) +} +func (tstep *Tstep) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, tstep.RegisterAddr, driverIndex, value) +} + +// Mscnt represents the Microstep Counter Register (0x6A) in the TMC2209 +// +// This register provides the actual microstep position within the microstep table. +// The value of MSCNT allows determination of the motor position within the electrical wave, +// which is essential for accurately controlling the motor's position during operation. +// +// Fields: +// - The register contains a 10-bit value indicating the actual position in the microstep table. +// - Range: 0 to 1023 (0x000 to 0x3FF) +type Mscnt struct { + Position uint32 + RegisterAddr uint8 +} + +func (mscnt *Mscnt) GetAddress() uint8 { + return mscnt.RegisterAddr +} + +// Pack the Position value (10-bit) into the Bytes field +func (mscnt *Mscnt) Pack() uint32 { + mscnt.Position = mscnt.Position & 0x03FF // Limit to 10 bits (0x3FF) + return mscnt.Position +} + +// Unpack the Bytes field into the Position field. +func (mscnt *Mscnt) Unpack(uint32) { + mscnt.Position = mscnt.Position & 0x03FF +} + +// NewMscnt initializes a new Mscnt struct with the correct register address. +func NewMscnt() *Mscnt { + return &Mscnt{ + RegisterAddr: MSCNT, // MSCNT register address + } +} +func (mscnt *Mscnt) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, mscnt.RegisterAddr) +} +func (mscnt *Mscnt) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, mscnt.RegisterAddr, driverIndex, value) +} + +// Mscuract MSCURACT represents the Microstep Current Register (0x6B) in the TMC2209 +// +// This register provides the actual current values for motor phases A and B as read from +// the internal sine wave table. The values are not scaled by the current setting but provide +// a raw value for the motor's electrical waves. +// +// Fields: +// - CUR_B (bits 0-7): The actual microstep current for motor phase B (signed value in the range +/-255). +// - CUR_A (bits 24-16): The actual microstep current for motor phase A (signed value in the range +/-255). +type Mscuract struct { + // Microstep current for phase B (sine wave) + CurB uint32 + // Microstep current for phase A (cosine wave) + CurA uint32 + // Register address + RegisterAddr uint8 +} + +func (mscuract *Mscuract) GetAddress() uint8 { + return mscuract.RegisterAddr +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (mscuract *Mscuract) Pack() uint32 { + mscuract.CurB = mscuract.CurB & 0xFF // Limit to 8 bits for CUR_B + mscuract.CurA = mscuract.CurA & 0xFF // Limit to 8 bits for CUR_A + return mscuract.CurA +} + +// Unpack the Bytes field into the individual fields. +func (mscuract *Mscuract) Unpack(uint32) { + mscuract.CurB = mscuract.CurB & 0xFF // Extract CUR_B (8 bits) + mscuract.CurA = (mscuract.CurA >> 16) & 0xFF // Extract CUR_A (8 bits) +} + +// NewMscuract initializes a new Mscuract struct with the correct register address. +func NewMscuract() *Mscuract { + return &Mscuract{ + RegisterAddr: MSCURACT, + } +} +func (mscuract *Mscuract) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, mscuract.RegisterAddr) +} +func (mscuract *Mscuract) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, mscuract.RegisterAddr, driverIndex, value) +} diff --git a/tmc2209/current.go b/tmc2209/current.go new file mode 100644 index 0000000..1224e6f --- /dev/null +++ b/tmc2209/current.go @@ -0,0 +1,42 @@ +package tmc2209 + +func Constrain(value, low, high uint32) uint32 { + if value < low { + return low + } + if value > high { + return high + } + return value +} + +func SetRunCurrent(percent uint8) { + _ = PercentToCurrentSetting(percent) + + // Set the run current register to runCurrent value +} + +func SetHoldCurrent(percent uint8) { + _ = PercentToCurrentSetting(percent) + // Set the hold current register to holdCurrent value +} +func PercentToCurrentSetting(percent uint8) uint8 { + constrainedPercent := Constrain(uint32(percent), 0, 100) + return uint8(Map(constrainedPercent, 0, 100, 0, 255)) +} + +func CurrentSettingToPercent(currentSetting uint8) uint8 { + return uint8(Map(uint32(currentSetting), 0, 255, 0, 100)) +} + +func PercentToHoldDelaySetting(percent uint8) uint8 { + constrainedPercent := Constrain(uint32(percent), 0, 100) + return uint8(Map(constrainedPercent, 0, 100, 0, 255)) +} + +func HoldDelaySettingToPercent(holdDelaySetting uint8) uint8 { + return uint8(Map(uint32(holdDelaySetting), 0, 255, 0, 100)) +} +func Map(value, fromLow, fromHigh, toLow, toHigh uint32) uint32 { + return (value-fromLow)*(toHigh-toLow)/(fromHigh-fromLow) + toLow +} diff --git a/tmc2209/motor_config.go b/tmc2209/motor_config.go new file mode 100644 index 0000000..c828e77 --- /dev/null +++ b/tmc2209/motor_config.go @@ -0,0 +1,37 @@ +package tmc2209 + +func SetMicrostepsPerStep(microsteps uint16) uint8 { + exponent := uint8(0) + microstepsShifted := microsteps >> 1 + + for microstepsShifted > 0 { + microstepsShifted = microstepsShifted >> 1 + exponent++ + } + + SetMicrostepsPerStepPowerOfTwo(exponent) + return exponent +} + +func SetMicrostepsPerStepPowerOfTwo(exponent uint8) { + switch exponent { + case 0: + // Set MRES_001 + case 1: + // Set MRES_002 + case 2: + // Set MRES_004 + case 3: + // Set MRES_008 + case 4: + // Set MRES_016 + case 5: + // Set MRES_032 + case 6: + // Set MRES_064 + case 7: + // Set MRES_128 + default: + // Set MRES_256 + } +} diff --git a/tmc2209/stealthchop.go b/tmc2209/stealthchop.go new file mode 100644 index 0000000..b9e1fe3 --- /dev/null +++ b/tmc2209/stealthchop.go @@ -0,0 +1,25 @@ +package tmc2209 + +func EnableStealthChop() { + // Set StealthChop enabled in the global config register +} + +func DisableStealthChop() { + // Set StealthChop disabled in the global config register +} + +func EnableCoolStep(lowerThreshold, upperThreshold uint8) { + // Enable CoolStep with specified thresholds +} + +func DisableCoolStep() { + // Disable CoolStep feature +} + +func EnableAutomaticCurrentScaling() { + // Enable Automatic Current Scaling +} + +func DisableAutomaticCurrentScaling() { + // Disable Automatic Current Scaling +} diff --git a/tmc2209/tmc2209.go b/tmc2209/tmc2209.go new file mode 100644 index 0000000..c00ec26 --- /dev/null +++ b/tmc2209/tmc2209.go @@ -0,0 +1,56 @@ +//go:build tinygo + +package tmc2209 + +import ( + "log" +) + +// TMC2209 represents a single TMC2209 stepper motor driver on the communication line. +type TMC2209 struct { + comm RegisterComm + address uint8 +} + +// NewTMC2209 creates a new instance of the TMC2209 driver for a specific address. +func NewTMC2209(comm RegisterComm, address uint8) *TMC2209 { + return &TMC2209{ + comm: comm, + address: address, + } +} + +// Setup initializes the communication interface with the TMC2209. +func (driver *TMC2209) Setup() error { + // Check if comm is of type *UARTComm + if uartComm, ok := driver.comm.(*UARTComm); ok { + // Call Setup only if comm is a *UARTComm + err := uartComm.Setup() + if err != nil { + return CustomError("Failed to setup UART communication: " + err.Error()) + } + } else { + // If it's not a UARTComm, log that it's using a different communication method + log.Println("Using a non-UART communication method") + } + + return nil +} + +// WriteRegister sends a register write command to the TMC2209. +func (driver *TMC2209) WriteRegister(reg uint8, value uint32) error { + if driver.comm == nil { + return CustomError("communication interface not set") + } + // Use the communication interface (RegisterComm) to write the register + return driver.comm.WriteRegister(reg, value, driver.address) +} + +// ReadRegister sends a register read command to the TMC2209 and returns the read value. +func (driver *TMC2209) ReadRegister(reg uint8) (uint32, error) { + if driver.comm == nil { + return 0, CustomError("communication interface not set") + } + // Use the communication interface (RegisterComm) to read the register + return driver.comm.ReadRegister(reg, driver.address) +} diff --git a/tmc2209/uartcomm.go b/tmc2209/uartcomm.go new file mode 100644 index 0000000..e99947f --- /dev/null +++ b/tmc2209/uartcomm.go @@ -0,0 +1,120 @@ +//go:build tinygo + +package tmc2209 + +import ( + "machine" + "time" +) + +// CustomError is a lightweight error type used for TinyGo compatibility. +type CustomError string + +func (e CustomError) Error() string { + return string(e) +} + +// UARTComm implements RegisterComm for UART-based communication +type UARTComm struct { + uart machine.UART + address uint8 +} + +// NewUARTComm creates a new UARTComm instance. +func NewUARTComm(uart machine.UART, address uint8) *UARTComm { + return &UARTComm{ + uart: uart, + address: address, + } +} + +// Setup initializes the UART communication with the TMC2209. +func (comm *UARTComm) Setup() error { + // Check if UART is initialized + if comm.uart == (machine.UART{}) { + return CustomError("UART not initialized") + } + + // Configure the UART interface with the desired baud rate and settings + err := comm.uart.Configure(machine.UARTConfig{ + BaudRate: 115200, + }) + if err != nil { + return CustomError("Failed to configure UART") + } + + // No built-in timeout in TinyGo, so timeout will be handled in the read/write methods + return nil +} + +// WriteRegister sends a register write command to the TMC2209 with a timeout. +func (comm *UARTComm) WriteRegister(register uint8, value uint32, driverIndex uint8) error { + buffer := []byte{ + 0x05, // Sync byte + comm.address, // Slave address + register | 0x80, // Write command (set MSB to 1 for write) + byte((value >> 24) & 0xFF), // MSB of value + byte((value >> 16) & 0xFF), // Middle byte + byte((value >> 8) & 0xFF), // Next byte + byte(value & 0xFF), // LSB of value + } + + // Calculate checksum by XORing all bytes + checksum := byte(0) + for _, b := range buffer[:7] { + checksum ^= b + } + buffer[7] = checksum // Set checksum byte + + // Write the data to the TMC2209 + done := make(chan error, 1) + + go func() { + comm.uart.Write(buffer) + done <- nil + }() + + // Implementing timeout using a 100ms timer + select { + case err := <-done: + return err + case <-time.After(100 * time.Millisecond): // Timeout after 100ms + return CustomError("write timeout") + } +} + +// ReadRegister sends a register read command to the TMC2209 with a timeout. +func (comm *UARTComm) ReadRegister(register uint8, driverIndex uint8) (uint32, error) { + var writeBuffer [4]byte + writeBuffer[0] = 0x05 // Sync byte + writeBuffer[1] = 0x00 // Slave address + writeBuffer[2] = register & 0x7F // Read command (MSB clear for read) + writeBuffer[3] = writeBuffer[0] ^ writeBuffer[1] ^ writeBuffer[2] // Checksum + + // Send the read command + done := make(chan []byte, 1) + go func() { + comm.uart.Write(writeBuffer[:]) + readBuffer := make([]byte, 8) + comm.uart.Read(readBuffer) + done <- readBuffer + }() + + // Implementing timeout using a 100ms timer + select { + case readBuffer := <-done: + // Validate checksum + checksum := byte(0) + for i := 0; i < 7; i++ { + checksum ^= readBuffer[i] + } + if checksum != readBuffer[7] { + return 0, CustomError("checksum error") + } + + // Return the value from the register + return uint32(readBuffer[3])<<24 | uint32(readBuffer[4])<<16 | uint32(readBuffer[5])<<8 | uint32(readBuffer[6]), nil + case <-time.After(100 * time.Millisecond): // Timeout after 100ms + return 0, CustomError("read timeout") + } +} diff --git a/tmc2209/utils.go b/tmc2209/utils.go new file mode 100644 index 0000000..4aca832 --- /dev/null +++ b/tmc2209/utils.go @@ -0,0 +1,73 @@ +package tmc2209 + +import "log" + +func CalculateCRC(data []byte) uint8 { + crc := uint8(0) + for _, byte := range data { + for i := 0; i < 8; i++ { + if (crc>>7)^(byte&0x01) == 1 { + crc = (crc << 1) ^ 0x07 + } else { + crc = crc << 1 + } + byte >>= 1 + } + } + return crc +} + +// VerifyCommunication checks the communication with the TMC2209 by reading the version register (IOIN). +// It returns true if the communication is successful (i.e., the version matches the expected version). +// VerifyCommunication verifies the communication with the TMC2209 by reading the version register (IOIN). +// It explicitly resets the struct and de-references it after the check to ensure memory is managed manually. +func VerifyCommunication(comm RegisterComm, driverIndex uint8) bool { + var io *Ioin + if io == nil { + io = NewIoin() // Initialize the struct if not already initialized + } else { + *io = Ioin{} + } + _, err := ReadRegister(comm, driverIndex, io.GetAddress()) + if err != nil { + return false + } + if io.Version == expectedVersion { + io = nil + return true + } + io = nil + return false +} + +// CheckErrorStatus verifies the communication and checks for error flags in the TMC2209 driver status. +// It explicitly resets the struct and de-references it when done to ensure memory is managed manually. +func CheckErrorStatus(comm RegisterComm, driverIndex uint8) bool { + var d *DrvStatus + if d == nil { + d = NewDrvStatus() + } else { + *d = DrvStatus{} + } + _, err := d.Read(comm, driverIndex) + if err != nil { + return false + } + errorFlags := d.Ola | d.S2vsa | d.S2vsb | d.Ot | d.S2ga | d.S2gb | d.Olb + if errorFlags != 0 { + log.Printf("TMC2209 Error Detected: %X", errorFlags) + return false + } + d = nil + return true +} + +// GetInterfaceTransmissionCount reads the IFCNT register to check for UART transmission status +func GetInterfaceTransmissionCount(comm RegisterComm, driverIndex uint8) (uint32, error) { + ifcnt := NewIfcnt() + _, err := ReadRegister(comm, driverIndex, ifcnt.GetAddress()) + if err != nil { + return 0, err + } + return ifcnt.Bytes, nil +} diff --git a/tmc2209/velocity.go b/tmc2209/velocity.go new file mode 100644 index 0000000..d3c1bb6 --- /dev/null +++ b/tmc2209/velocity.go @@ -0,0 +1,12 @@ +package tmc2209 + +func MoveAtVelocity(microstepsPerPeriod int32) { + // Write velocity to the relevant register +} +func MoveUsingStepDirInterface() { + // Set the interface to use step/direction interface +} +func GetVelocity() uint32 { + // Read the actual velocity register + return 0 // Placeholder return value +} From ccfcfa837ebf21789527796e94ee047e4e71598b Mon Sep 17 00:00:00 2001 From: John Hobbs Date: Wed, 12 Feb 2025 16:52:11 -0600 Subject: [PATCH 056/137] Add MAX6675 device --- max6675/max6675.go | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 max6675/max6675.go diff --git a/max6675/max6675.go b/max6675/max6675.go new file mode 100644 index 0000000..9d2433e --- /dev/null +++ b/max6675/max6675.go @@ -0,0 +1,51 @@ +// Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/max6675.pdf +package max6675 + +import ( + "errors" + "machine" +) + +// ErrThermocoupleOpen is returned when the thermocouple input is open. +// i.e. not attached or faulty +var ErrThermocoupleOpen = errors.New("thermocouple input open") + +type Device struct { + bus machine.SPI + cs machine.Pin +} + +// Create a new Device to read from a MAX6675 thermocouple. +// Pins must be configured before use. Frequency for SPI +// should be 4.3MHz maximum. +func NewDevice(bus machine.SPI, cs machine.Pin) *Device { + return &Device{ + bus: bus, + cs: cs, + } +} + +// Read and return the temperature in celsius +func (d *Device) Read() (float32, error) { + var ( + read []byte = []byte{0, 0} + value uint16 + ) + + d.cs.Low() + if err := d.bus.Tx([]byte{0, 0}, read); err != nil { + return 0, err + } + d.cs.High() + + // datasheet: Bit D2 is normally low and goes high if the thermocouple input is open. + if read[1]&0x04 == 0x04 { + return 0, ErrThermocoupleOpen + } + + // data is 12 bits, split across the two bytes + // -XXXXXXX XXXXX--- + value = (uint16(read[0]) << 5) | (uint16(read[1]) >> 3) + + return float32(value) * 0.25, nil +} From 6281f36662a1a13b49d90074d195defb11dce6ff Mon Sep 17 00:00:00 2001 From: John Hobbs Date: Wed, 12 Feb 2025 17:10:32 -0600 Subject: [PATCH 057/137] MAX6675 example & smoke test --- examples/max6675/main.go | 34 ++++++++++++++++++++++++++++++++++ smoketest.sh | 1 + 2 files changed, 35 insertions(+) create mode 100644 examples/max6675/main.go diff --git a/examples/max6675/main.go b/examples/max6675/main.go new file mode 100644 index 0000000..b583088 --- /dev/null +++ b/examples/max6675/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "machine" + "time" + + "tinygo.org/x/drivers/max6675" +) + +// example for reading temperature from a thermocouple +func main() { + // Pins are for an Adafruit Feather nRF52840 Express + machine.D5.Configure(machine.PinConfig{Mode: machine.PinOutput}) + machine.D5.High() + + machine.SPI0.Configure(machine.SPIConfig{ + Frequency: 1_000_000, + SCK: machine.SPI0_SCK_PIN, + SDI: machine.SPI0_SDI_PIN, + }) + + thermocouple := max6675.NewDevice(machine.SPI0, machine.D5) + + for { + temp, err := thermocouple.Read() + if err != nil { + println(err) + return + } + fmt.Printf("%0.02f C : %0.02f F\n", temp, (temp*9/5)+32) + time.Sleep(time.Second) + } +} diff --git a/smoketest.sh b/smoketest.sh index 50a6440..eec70b1 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -139,6 +139,7 @@ tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mcp9808/mai tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/tmc2209/main.go tinygo build -size short -o ./build/test.hex -target=pico ./examples/tmc5160/main.go 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 # network examples (espat) tinygo build -size short -o ./build/test.hex -target=challenger-rp2040 ./examples/net/ntpclient/ # network examples (wifinina) From 27a7d5031e87e9fe26aac93205dcbd42479599bf Mon Sep 17 00:00:00 2001 From: Scott Feldman Date: Tue, 18 Feb 2025 14:34:09 -0800 Subject: [PATCH 058/137] move to latest golang.org/x/net v0.33.0 (#732) v0.33.0 has fixes for: CVE-2024-45338: Non-linear parsing of case-insensitive content in golang.org/x/net/html CVE-2023-45288: net/http, x/net/http2: close connections when receiving too many headers Note: requires changes from tinygo PR#4685. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7762179..10c3e98 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/orsinium-labs/tinymath v1.1.0 github.com/soypat/natiu-mqtt v0.5.1 golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d - golang.org/x/net v0.7.0 + golang.org/x/net v0.33.0 tinygo.org/x/tinyfont v0.3.0 tinygo.org/x/tinyterm v0.1.0 ) diff --git a/go.sum b/go.sum index 0d18cdf..6bb3557 100644 --- a/go.sum +++ b/go.sum @@ -20,8 +20,8 @@ github.com/soypat/natiu-mqtt v0.5.1/go.mod h1:xEta+cwop9izVCW7xOx2W+ct9PRMqr0gNV github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= tinygo.org/x/drivers v0.14.0/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= tinygo.org/x/drivers v0.15.1/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= From 06dd60fba2b24144700494ebd6d298c5bc57de74 Mon Sep 17 00:00:00 2001 From: Scott Feldman Date: Fri, 21 Feb 2025 02:24:15 -0800 Subject: [PATCH 059/137] Add wifi driver comboat for Elecrow W5 rp2040 and rp2350 devices (#741) * Add wifi driver comboat for Elecrow W5 rp2040 and rp2350 devices Combo-AT driver uses AT command set to talk to onboard rtl8720d wifi device. The driver supports UDP/TCP/TLS clients in Wifi STA mode. Support for UDP/TCP servers is not supported, currently. https://aithinker-combo-guide.readthedocs.io/en/latest/docs/instruction/index.html https://aithinker-combo-guide.readthedocs.io/en/latest/docs/command-set/index.html https://aithinker-combo-guide.readthedocs.io/en/latest/docs/command-examples/index.html * Add wifi driver comboat for Elecrow W5 rp2040 and rp2350 devices Combo-AT driver uses AT command set to talk to onboard rtl8720d wifi device. The driver supports UDP/TCP/TLS clients in Wifi STA mode. Support for UDP/TCP servers is not supported, currently. https://aithinker-combo-guide.readthedocs.io/en/latest/docs/instruction/index.html https://aithinker-combo-guide.readthedocs.io/en/latest/docs/command-set/index.html https://aithinker-combo-guide.readthedocs.io/en/latest/docs/command-examples/index.html * switch to comboat_fw build tag for examples/net's that work with comboat driver --- comboat/comboat.go | 711 ++++++++++++++++++++++++++ comboat/errors.go | 86 ++++ examples/net/http-get/main.go | 2 +- examples/net/http-head/main.go | 2 +- examples/net/http-post/main.go | 2 +- examples/net/http-postform/main.go | 2 +- examples/net/mqttclient/natiu/main.go | 2 +- examples/net/mqttclient/paho/main.go | 2 +- examples/net/ntpclient/main.go | 2 +- examples/net/snake/init.go | 2 +- examples/net/snake/main.go | 2 +- examples/net/socket/main.go | 2 +- examples/net/tcpclient/main.go | 2 +- examples/net/tlsclient/main.go | 2 +- examples/net/webclient/main.go | 2 +- examples/net/websocket/dial/main.go | 2 +- netlink/probe/comboat.go | 26 + smoketest.sh | 3 + 18 files changed, 840 insertions(+), 14 deletions(-) create mode 100644 comboat/comboat.go create mode 100644 comboat/errors.go create mode 100644 netlink/probe/comboat.go diff --git a/comboat/comboat.go b/comboat/comboat.go new file mode 100644 index 0000000..2687c36 --- /dev/null +++ b/comboat/comboat.go @@ -0,0 +1,711 @@ +// Package comboat implements WiFi driver for the Aithinker-Combo-AT WiFi +// device found on the Elecrow W5 rp2040 and rp2350 devices. Ths WiFi device +// is a RTL8720d variant. The driver interface is via AT command set over UART +// (see reference docs below). +// +// NOTE: the driver doesn't support UDP/TCP server connections in STA mode, +// currently. UDP/TCP/TLS client connections are supported in STA mode. +// +// https://aithinker-combo-guide.readthedocs.io/en/latest/docs/instruction/index.html +// https://aithinker-combo-guide.readthedocs.io/en/latest/docs/command-set/index.html +// https://aithinker-combo-guide.readthedocs.io/en/latest/docs/command-examples/index.html + +package comboat // import "tinygo.org/x/drivers/comboat" + +import ( + "bytes" + "errors" + "fmt" + "io" + "machine" + "net" + "net/netip" + "strconv" + "sync" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/netdev" + "tinygo.org/x/drivers/netlink" +) + +type Config struct { + BaudRate uint32 + Uart *machine.UART + Tx machine.Pin + Rx machine.Pin +} + +type socket struct { + protocol int + id string + rx chan []byte + remainder []byte + laddr netip.AddrPort // Set in Bind() +} + +type device struct { + cfg *Config + uart *machine.UART + uartMu sync.Mutex + mac net.HardwareAddr + ip netip.Addr + gateway netip.Addr + buf [1500]byte + pos int + last []byte + ok chan bool + txReady chan bool + accept chan string + err chan error + sockets [8]*socket + sync.Mutex +} + +func NewDevice(cfg *Config) *device { + return &device{ + cfg: cfg, + ok: make(chan bool), + txReady: make(chan bool), + accept: make(chan string), + err: make(chan error), + } +} + +func logDebug(msg string) { + //println("[DEBUG] " + msg) +} + +func logError(msg string) { + println("[ERROR] " + msg) +} + +func split(resp []byte, part int, del, on string) string { + parts := bytes.Split(resp, []byte(del)) + if part >= len(parts) { + return "Split parts error getting " + on + } + return string(parts[part]) +} + +func (d *device) getFWVersion() string { + return split(d.last, 1, ":", "FW version") +} + +func (d *device) saveMAC() { + raw := split(d.last, 1, ":", "MAC") + if len(raw) > 11 { + macStr := fmt.Sprintf("%s:%s:%s:%s:%s:%s", + raw[0:2], raw[2:4], raw[4:6], + raw[6:8], raw[8:10], raw[10:12]) + d.mac, _ = net.ParseMAC(macStr) + } +} + +var countryCodes = map[int]string{ + 1: "JP Japan", + 2: "American Samoa", + 3: "CA Canada", + 4: "US", + 5: "CN China", + 6: "Hong Kong, China", + 7: "Taiwan, China", + 8: "MO Macau, China", + 9: "IL Israel", + 10: "Singapore", + 11: "KR South Korea", + 12: "TR Türkiye", + 13: "AU Australia", + 14: "ZA South Africa", + 15: "BR Brazil", +} + +func (d *device) getCountry() (code string) { + code = split(d.last, 1, ":", "county code") + codeNum, err := strconv.Atoi(code) + if err != nil { + return + } + if val, ok := countryCodes[codeNum]; ok { + code = val + } + return +} + +func (d *device) saveIP() { + ipStr := split(d.last, 7, ",", "IP address") + gwStr := split(d.last, 8, ",", "gateway address") + d.ip, _ = netip.ParseAddr(ipStr) + d.gateway, _ = netip.ParseAddr(gwStr) +} + +func (d *device) execute(cmd string, timeout int) (err error) { + logDebug("EXECUTE " + cmd) + + d.uartMu.Lock() + _, err = d.uart.Write([]byte(cmd + "\r\n")) + d.uartMu.Unlock() + + if err != nil { + return + } + + t := time.NewTicker(time.Duration(timeout) * time.Millisecond) + defer t.Stop() + + select { + case <-t.C: + return errors.New("Timed out") + case <-d.ok: + return + case err = <-d.err: + return + } +} + +func (d *device) send(cmd string, timeout int) (err error) { + logDebug("EXECUTE " + cmd) + + d.uartMu.Lock() + _, err = d.uart.Write([]byte(cmd + "\r\n")) + d.uartMu.Unlock() + + if err != nil { + return + } + + t := time.NewTicker(time.Duration(timeout) * time.Millisecond) + defer t.Stop() + + select { + case <-t.C: + return errors.New("Timed out") + case <-d.txReady: + return + case err = <-d.err: + return + } +} + +func (d *device) findSocket(id string) (*socket, error) { + for _, s := range d.sockets { + if s.id == id { + return s, nil + } + } + return nil, errors.New("Socket not found with id: " + id) +} + +func (d *device) getSocket(sockfd int) (*socket, error) { + if sockfd < 0 || sockfd+1 > len(d.sockets) { + return nil, netdev.ErrInvalidSocketFd + } + if d.sockets[sockfd] == nil { + return nil, netdev.ErrInvalidSocketFd + } + return d.sockets[sockfd], nil +} + +func (d *device) handle(event []byte) { + logDebug("GOT EVENT " + string(event)) + switch { + + // SocketDisconnect, + case bytes.HasPrefix(event, []byte("SocketDisconnect")): + id := split(event, 1, ",", "SocketDisconnect") + s, err := d.findSocket(id) + if err == nil { + close(s.rx) // Sends io.EOF + } + + // SocketSeed,, + case bytes.HasPrefix(event, []byte("SocketSeed,2,1")): + //d.uart.Write([]byte("AT+SOCKET?" + "\r\n")) + } +} + +func (d *device) processUART() { + + if d.pos == 1 && d.buf[0] == '>' { + d.pos = 0 + logDebug("GOT >") + d.txReady <- true + } + + sofar := d.buf[:d.pos] + + if !bytes.HasSuffix(sofar, []byte("\r\n")) { + return + } + + // Strip CR/LF off end + sofar = sofar[:len(sofar)-2] + + switch { + + case bytes.HasPrefix(sofar, []byte("+EVENT:SocketDown")): + // +EVENT:SocketDown,,, + parts := bytes.SplitN(sofar, []byte(","), 4) + if len(parts) != 4 { + logError("Error parsing +EVENT:SocketDown: " + string(sofar)) + d.pos = 0 + return + } + id := string(parts[1]) + length, err := strconv.Atoi(string(parts[2])) + if err != nil { + logError("Error parsing length from: " + string(parts[2])) + d.pos = 0 + return + } + if length != len(parts[3]) { + // This can happen if actually contains a CR/LF. + // Return without resetting d.pos to continue reading + // in the full . + return + } + s, err := d.findSocket(id) + if err != nil { + logError(err.Error()) + d.pos = 0 + return + } + logDebug("GOT +EVENT:SocketDown," + id + "," + string(parts[2])) + d.pos = 0 + data := make([]byte, len(parts[3])) + copy(data, parts[3]) + s.rx <- data + + case bytes.HasPrefix(sofar, []byte("OK")): + d.pos = 0 + logDebug("GOT OK") + d.ok <- true + + case bytes.HasPrefix(sofar, []byte("ERROR")): + d.pos = 0 + logDebug("GOT ERROR") + errStr := getErrStr(d.last) + d.err <- errors.New(errStr) + + case bytes.HasPrefix(sofar, []byte("+EVENT:")): + d.pos = 0 + event := sofar[len("+EVENT:"):] + d.handle(event) + + default: + // Catch everything else and store in d.last + d.pos = 0 + size := len(sofar) + if size > 0 { + d.last = make([]byte, size) + copy(d.last, sofar[:size]) + logDebug("GOT LINE " + string(d.last)) + } + } +} + +func (d *device) serviceUART() { + for { + d.uartMu.Lock() + for d.uart.Buffered() > 0 { + if d.pos >= len(d.buf) { + println("Trying to write past buffer") + d.pos = 0 + break + } + var err error + d.buf[d.pos], err = d.uart.ReadByte() + if err == nil { + d.pos++ + d.processUART() + } + } + d.uartMu.Unlock() + time.Sleep(10 * time.Millisecond) + } +} + +func (d *device) NetConnect(params *netlink.ConnectParams) error { + + d.Lock() + defer d.Unlock() + + d.uart = d.cfg.Uart + d.uart.Configure(machine.UARTConfig{ + BaudRate: d.cfg.BaudRate, + TX: d.cfg.Tx, + RX: d.cfg.Rx, + }) + + go d.serviceUART() + + fmt.Printf("\r\n") + fmt.Printf("TinyGo Combo-AT WiFi network device driver\r\n") + + fmt.Printf("\r\n") + fmt.Printf("Driver version : %s\r\n", drivers.Version) + + if len(params.Ssid) == 0 { + return netlink.ErrMissingSSID + } + + // AT Test to see if device is alive + if err := d.execute("AT", 1000); err != nil { + return err + } + + // Disable echo + if err := d.execute("ATE0", 1000); err != nil { + return err + } + + // Get FW version + if err := d.execute("AT+GMR", 1000); err != nil { + return err + } + fmt.Printf("Combo-AT firmware version : %s\r\n", d.getFWVersion()) + + // Get/save MAC addresses + if err := d.execute("AT+CIPSTAMAC_DEF?", 1000); err != nil { + return err + } + d.saveMAC() + fmt.Printf("MAC address : %s\r\n", d.mac.String()) + + // Set country code US + if err := d.execute("AT+WCOUNTRY=4", 1000); err != nil { + return err + } + + // Get country code + if err := d.execute("AT+WCOUNTRY?", 1000); err != nil { + return err + } + fmt.Printf("WiFi country code : %s\r\n", d.getCountry()) + + // Set Wi-Fi working mode to STA and save to flash + if err := d.execute("AT+WMODE=1,1", 1000); err != nil { + return err + } + + // Connect to Wifi AP (keep trying until connected) + fmt.Printf("\r\n") + cmd := "AT+WJAP=" + params.Ssid + "," + params.Passphrase + + for { + fmt.Printf("Connecting to WiFi SSID '%s'...", params.Ssid) + if err := d.execute(cmd, 20000); err != nil { + fmt.Printf("FAILED (%s)\r\n", err.Error()) + continue + } + break + } + + fmt.Printf("CONNECTED\r\n") + + // Automatically reconnect to Wi-Fi after power on + if err := d.execute("AT+WAUTOCONN=1", 1000); err != nil { + return err + } + + // Get/save IP/gateway addresses + if err := d.execute("AT+WJAP?", 1000); err != nil { + return err + } + d.saveIP() + + fmt.Printf("\r\n") + fmt.Printf("DHCP-assigned IP : %s\r\n", d.ip) + fmt.Printf("DHCP-assigned gateway : %s\r\n", d.gateway) + fmt.Printf("\r\n") + + // Set socket receiving mode to active + if err := d.execute("AT+SOCKETRECVCFG=1", 1000); err != nil { + return err + } + + return nil +} + +func (d *device) NetDisconnect() { + d.Lock() + defer d.Unlock() + // Disconnect from WiFi AP + d.execute("AT+WDISCONNECT", 1000) +} + +func (d *device) NetNotify(cb func(netlink.Event)) { + fmt.Printf("\r\n%s\r\n", netlink.ErrNotSupported) +} + +func (d *device) GetHardwareAddr() (net.HardwareAddr, error) { + return d.mac, nil +} + +func (d *device) _getHostByName(name string) (ip netip.Addr, err error) { + if err = d.execute("AT+WDOMAIN="+name, 1000); err != nil { + return + } + ipStr := split(d.last, 1, ":", "host by name") + return netip.ParseAddr(ipStr) +} + +func (d *device) GetHostByName(name string) (ip netip.Addr, err error) { + + // If it's already a dotted-network address, and not a host name, + // return it + ip, err = netip.ParseAddr(name) + if err == nil { + return + } + + d.Lock() + defer d.Unlock() + + return d._getHostByName(name) +} + +func (d *device) Addr() (netip.Addr, error) { + return d.ip, nil +} + +func (d *device) Socket(domain, stype, protocol int) (int, error) { + + switch domain { + case netdev.AF_INET: + default: + return -1, netdev.ErrFamilyNotSupported + } + + switch { + case protocol == netdev.IPPROTO_TCP && stype == netdev.SOCK_STREAM: + case protocol == netdev.IPPROTO_TLS && stype == netdev.SOCK_STREAM: + case protocol == netdev.IPPROTO_UDP && stype == netdev.SOCK_DGRAM: + default: + return -1, netdev.ErrProtocolNotSupported + } + + d.Lock() + defer d.Unlock() + + // Search for empty slot in sockets array + for fd, s := range d.sockets { + if s == nil { + // Found one + d.sockets[fd] = &socket{ + protocol: protocol, + rx: make(chan []byte, 10), + } + return fd, nil + } + } + + return -1, netdev.ErrNoMoreSockets +} + +func (d *device) Bind(sockfd int, ip netip.AddrPort) error { + + d.Lock() + defer d.Unlock() + + s, err := d.getSocket(sockfd) + if err != nil { + return err + } + + s.laddr = ip + return nil +} + +func (d *device) Connect(sockfd int, host string, ip netip.AddrPort) error { + + var addr string + var cmd string + + d.Lock() + defer d.Unlock() + + s, err := d.getSocket(sockfd) + if err != nil { + return err + } + + if host == "" { + addr = ip.Addr().String() + } else { + ip, err := d._getHostByName(host) + if err != nil { + return err + } + addr = ip.String() + } + port := strconv.Itoa(int(ip.Port())) + + switch s.protocol { + case netdev.IPPROTO_UDP: + cmd = "AT+SOCKET=2," + addr + "," + port + case netdev.IPPROTO_TCP: + cmd = "AT+SOCKET=4," + addr + "," + port + case netdev.IPPROTO_TLS: + cmd = "AT+SOCKET=7," + addr + "," + port + } + + if cmd == "" { + return netdev.ErrProtocolNotSupported + } + + if err := d.execute(cmd, 20000); err != nil { + return err + } + + s.id = split(d.last, 1, "=", "connection ID") + + return nil +} + +func (d *device) Listen(sockfd, backlog int) error { + + // TODO Creating a TCP server socket isn't working when in STA mode, + // TODO returning error "Socket bind error". + // TODO The reference example shows a TCP server example in AP mode. + + /* + var cmd string + + d.Lock() + defer d.Unlock() + + s, err := d.getSocket(sockfd) + if err != nil { + return err + } + + port := strconv.Itoa(int(s.laddr.Port())) + + switch s.protocol { + case netdev.IPPROTO_UDP: + cmd = "AT+SOCKET=1," + port + case netdev.IPPROTO_TCP: + cmd = "AT+SOCKET=3," + port + } + + if cmd == "" { + return netdev.ErrProtocolNotSupported + } + + if err := d.execute(cmd, 20000); err != nil { + return err + } + + s.id = split(d.last, 1, "=", "connection ID") + */ + + return netdev.ErrNotSupported +} + +func (d *device) Accept(sockfd int) (int, netip.AddrPort, error) { + return 0, netip.AddrPort{}, netdev.ErrNotSupported +} + +func (d *device) Send(sockfd int, buf []byte, flags int, deadline time.Time) (int, error) { + + d.Lock() + defer d.Unlock() + + s, err := d.getSocket(sockfd) + if err != nil { + return 0, err + } + + cmd := fmt.Sprintf("AT+SOCKETSEND=%s,%d", s.id, len(buf)) + + if err := d.send(cmd, 1000); err != nil { + return 0, err + } + + // AT+SOCKETSEND will sub-packet send data into 1024-byte chunks, + // automatically, so send the full buffer in one shot, even if it's + // bigger than 1024 bytes. + + d.uartMu.Lock() + n, err := d.uart.Write(buf) + d.uartMu.Unlock() + + if err != nil { + return 0, err + } + + // Expecting "OK" after good send, or "ERROR" + + t := time.NewTicker(time.Duration(1000) * time.Millisecond) + defer t.Stop() + + select { + case <-t.C: + return 0, errors.New("Timed out") + case <-d.ok: + return n, nil + case err = <-d.err: + return 0, err + } +} + +func (d *device) Recv(sockfd int, buf []byte, flags int, deadline time.Time) (int, error) { + + d.Lock() + defer d.Unlock() + + s, err := d.getSocket(sockfd) + if err != nil { + return 0, err + } + + // 1. Use leftover data first + if len(s.remainder) > 0 { + n := copy(buf, s.remainder) + s.remainder = s.remainder[n:] + return n, nil + } + + // 2. Get new data from the channel + data, ok := <-s.rx + if !ok { + // Socket closed, return EOF + return 0, io.EOF + } + + // 3. Copy data, handle leftovers + n := copy(buf, data) + if n < len(data) { + s.remainder = data[n:] + } + + return n, nil +} + +func (d *device) Close(sockfd int) error { + + d.Lock() + defer d.Unlock() + + s, err := d.getSocket(sockfd) + if err != nil { + return err + } + + // Delete socket only if connection was successful (s.id is set) + if s.id != "" { + cmd := fmt.Sprintf("AT+SOCKETDEL=%s", s.id) + if err = d.execute(cmd, 1000); err != nil { + return err + } + } + + d.sockets[sockfd] = nil + + return nil +} + +func (d *device) SetSockOpt(sockfd, level, opt int, value interface{}) error { + return netdev.ErrNotSupported +} diff --git a/comboat/errors.go b/comboat/errors.go new file mode 100644 index 0000000..5b1bf6d --- /dev/null +++ b/comboat/errors.go @@ -0,0 +1,86 @@ +package comboat + +import ( + "bytes" + "strconv" +) + +var errStrings = map[int]string{ + + // System framework related error codes + + 0: "success", + 1: "The command is not supported (the combo framework contains the command but the current platform has not transplanted or adapted to support it)", + 2: "The command parameters contain unsupported operations (the current platform only supports some operations for this command)", + 3: "The instruction format is incorrect (this refers to the wrong number of parameters, for example, two parameters are required, but only one parameter is entered)", + 4: "Parameter error (the content of the parameter is wrong, for example, a number between 0 and 9 is required, but 10 or xyz is passed in, which is a parameter error)", + 5: "Parameter length error (command length exceeds the maximum supported length)", + 31: "The current command has not ended and needs to report the status asynchronously. This value is used by the state machine to determine the use of the command and no message is returned.", + 32: "Unknown error (or unhandled error type)", + + // Common error codes + + 33: "malloc error", + 34: "Failed to read buf", + 35: "Failed to write buf", + 36: "Configuration error (configuration error loaded from memory, for example, we set port -1 for OTA upgrade, and check port error when executing AT+OTA, then configuration error will be reported)", + 37: "Failed to create task", + 38: "Flash read and write failure", + 39: "Serial port configuration error, unsupported baud rate", + 40: "Serial port configuration error, unsupported data bits", + 41: "Serial port configuration error, unsupported stop bit", + 42: "Serial port configuration error, unsupported parity bit", + 43: "Serial port configuration error, unsupported flow control", + 44: "Serial port configuration failed", + 45: "Wrong username/password", + 46: "Low power mode error or unsupported low power mode", + 47: "Uninitialized configuration data error (including io mapping data)", + 63: "General error code (without other information)", + + // Wi-Fi related error codes + + 64: "Wi-Fi not initialized or initialization failed", + 65: "Wi-Fi mode error (unable to connect to Wi-Fi in single AP mode)", + 66: "Wi-Fi connection failed", + 67: "Wi-Fi connection successful, error in obtaining IP (DHCP)", + 68: "Failed to obtain encryption method", + 69: "The specified AP was not found.", + 70: "Wi-Fi scan start failed", + 71: "Wi-Fi scan timeout", + 72: "Failed to enable AP hotspot", + 73: "Failed to obtain the Wi-Fi information of the router or the AP information that you enabled yourself", + 74: "The network card (STA/AP) is not running", + 75: "Wi-Fi country code error (unsupported Wi-Fi country code)", + 76: "The current network configuration mode is wrong.", + 95: "Wi-Fi connection unknown error", + + // Socket related error codes + + 96: "Failed to create socket", + 97: "Socket connection failed", + 98: "DNS Failure", + 99: "The socket status is wrong (for example, TCP is not connected yet)", + 100: "Socket type error", + 101: "Socket send failed", + 102: "Socket receive failed", + 103: "Socket monitoring thread creation failed", + 104: "Socket bind error", + 105: "The current connection cannot be transparently linked (wrong socket type or number)", + 106: "PING test failed (all packets lost)", + 107: "Wi-Fi country code error (unsupported Wi-Fi country code)", + 108: "SSL Config Error", + 109: "SSL verification error (usually caused by unsupported SSL encryption type or certificate error)", + 127: "Unknown socket error", +} + +func getErrStr(errLine []byte) (errStr string) { + errStr = "Can't parse ERROR response" + tokens := bytes.Split(errLine, []byte(":")) + if len(tokens) > 1 { + errCode, err := strconv.Atoi(string(tokens[1])) + if err == nil { + errStr = errStrings[errCode] + } + } + return +} diff --git a/examples/net/http-get/main.go b/examples/net/http-get/main.go index 4359f8d..aa51acc 100644 --- a/examples/net/http-get/main.go +++ b/examples/net/http-get/main.go @@ -9,7 +9,7 @@ // examples/net/webclient (for HTTP) // examples/net/tlsclient (for HTTPS) -//go:build ninafw || wioterminal +//go:build ninafw || wioterminal || comboat_fw package main diff --git a/examples/net/http-head/main.go b/examples/net/http-head/main.go index 355e9d4..fe90397 100644 --- a/examples/net/http-head/main.go +++ b/examples/net/http-head/main.go @@ -9,7 +9,7 @@ // examples/net/webclient (for HTTP) // examples/net/tlsclient (for HTTPS) -//go:build ninafw || wioterminal +//go:build ninafw || wioterminal || comboat_fw package main diff --git a/examples/net/http-post/main.go b/examples/net/http-post/main.go index 8af7516..b5f55b1 100644 --- a/examples/net/http-post/main.go +++ b/examples/net/http-post/main.go @@ -9,7 +9,7 @@ // examples/net/webclient (for HTTP) // examples/net/tlsclient (for HTTPS) -//go:build ninafw || wioterminal +//go:build ninafw || wioterminal || comboat_fw package main diff --git a/examples/net/http-postform/main.go b/examples/net/http-postform/main.go index 03f87f9..c5757fa 100644 --- a/examples/net/http-postform/main.go +++ b/examples/net/http-postform/main.go @@ -9,7 +9,7 @@ // examples/net/webclient (for HTTP) // examples/net/tlsclient (for HTTPS) -//go:build ninafw || wioterminal +//go:build ninafw || wioterminal || comboat_fw package main diff --git a/examples/net/mqttclient/natiu/main.go b/examples/net/mqttclient/natiu/main.go index 5c82811..84111f5 100644 --- a/examples/net/mqttclient/natiu/main.go +++ b/examples/net/mqttclient/natiu/main.go @@ -4,7 +4,7 @@ // Note: It may be necessary to increase the stack size when using // paho.mqtt.golang. Use the -stack-size=4KB command line option. -//go:build ninafw || wioterminal || challenger_rp2040 +//go:build ninafw || wioterminal || challenger_rp2040 || comboat_fw package main diff --git a/examples/net/mqttclient/paho/main.go b/examples/net/mqttclient/paho/main.go index ac39a2c..a080b0d 100644 --- a/examples/net/mqttclient/paho/main.go +++ b/examples/net/mqttclient/paho/main.go @@ -4,7 +4,7 @@ // Note: It may be necessary to increase the stack size when using // paho.mqtt.golang. Use the -stack-size=4KB command line option. -//go:build ninafw || wioterminal || challenger_rp2040 +//go:build ninafw || wioterminal || challenger_rp2040 || comboat_fw package main diff --git a/examples/net/ntpclient/main.go b/examples/net/ntpclient/main.go index da54c5d..9ea079d 100644 --- a/examples/net/ntpclient/main.go +++ b/examples/net/ntpclient/main.go @@ -3,7 +3,7 @@ // It creates a UDP connection to request the current time and parse the // response from a NTP server. The system time is set to NTP time. -//go:build ninafw || wioterminal || challenger_rp2040 +//go:build ninafw || wioterminal || challenger_rp2040 || comboat_fw package main diff --git a/examples/net/snake/init.go b/examples/net/snake/init.go index a00a814..641ecd5 100644 --- a/examples/net/snake/init.go +++ b/examples/net/snake/init.go @@ -1,4 +1,4 @@ -//go:build ninafw || wioterminal +//go:build ninafw || wioterminal || comboat_fw package main diff --git a/examples/net/snake/main.go b/examples/net/snake/main.go index 40bfc65..a261fc1 100644 --- a/examples/net/snake/main.go +++ b/examples/net/snake/main.go @@ -31,7 +31,7 @@ // func. This forces segments to connect and run concurrently, which is a good // test of the underlying driver's ability to handle concurrent connections. -//go:build ninafw || wioterminal +//go:build ninafw || wioterminal || comboat_fw package main diff --git a/examples/net/socket/main.go b/examples/net/socket/main.go index f55b053..2e867fd 100644 --- a/examples/net/socket/main.go +++ b/examples/net/socket/main.go @@ -4,7 +4,7 @@ // // nc -lk 8080 -//go:build ninafw || wioterminal || challenger_rp2040 +//go:build ninafw || wioterminal || challenger_rp2040 || comboat_fw package main diff --git a/examples/net/tcpclient/main.go b/examples/net/tcpclient/main.go index 9ef9820..24f0775 100644 --- a/examples/net/tcpclient/main.go +++ b/examples/net/tcpclient/main.go @@ -5,7 +5,7 @@ // // nc -lk 8080 -//go:build ninafw || wioterminal || challenger_rp2040 || pico +//go:build ninafw || wioterminal || challenger_rp2040 || comboat_fw package main diff --git a/examples/net/tlsclient/main.go b/examples/net/tlsclient/main.go index 46cd110..adef220 100644 --- a/examples/net/tlsclient/main.go +++ b/examples/net/tlsclient/main.go @@ -5,7 +5,7 @@ // // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security -//go:build ninafw || wioterminal +//go:build ninafw || wioterminal || comboat_fw package main diff --git a/examples/net/webclient/main.go b/examples/net/webclient/main.go index 43096bf..6b28783 100644 --- a/examples/net/webclient/main.go +++ b/examples/net/webclient/main.go @@ -17,7 +17,7 @@ // } // --------------------------------------------------------------------------- -//go:build ninafw || wioterminal +//go:build ninafw || wioterminal || comboat_fw package main diff --git a/examples/net/websocket/dial/main.go b/examples/net/websocket/dial/main.go index 4aa4b8e..02e17da 100644 --- a/examples/net/websocket/dial/main.go +++ b/examples/net/websocket/dial/main.go @@ -6,7 +6,7 @@ // Note: It may be necessary to increase the stack size when using // "golang.org/x/net/websocket". Use the -stack-size=4KB command line option. -//go:build ninafw || wioterminal +//go:build ninafw || wioterminal || comboat_fw package main diff --git a/netlink/probe/comboat.go b/netlink/probe/comboat.go new file mode 100644 index 0000000..c1ed007 --- /dev/null +++ b/netlink/probe/comboat.go @@ -0,0 +1,26 @@ +//go:build comboat_fw + +package probe + +import ( + "machine" + + "tinygo.org/x/drivers/comboat" + "tinygo.org/x/drivers/netdev" + "tinygo.org/x/drivers/netlink" +) + +func Probe() (netlink.Netlinker, netdev.Netdever) { + + cfg := comboat.Config{ + BaudRate: 115200, + Uart: machine.UART1, + Tx: machine.UART1_TX_PIN, + Rx: machine.UART1_RX_PIN, + } + + combo := comboat.NewDevice(&cfg) + netdev.UseNetdev(combo) + + return combo, combo +} diff --git a/smoketest.sh b/smoketest.sh index eec70b1..f21e2be 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -154,3 +154,6 @@ tinygo build -size short -o ./build/test.hex -target=nano-rp2040 -stack-size 8kb tinygo build -size short -o ./build/test.hex -target=wioterminal -stack-size 8kb ./examples/net/webclient/ tinygo build -size short -o ./build/test.hex -target=wioterminal -stack-size 8kb ./examples/net/webserver/ tinygo build -size short -o ./build/test.hex -target=wioterminal -stack-size 8kb ./examples/net/mqttclient/paho/ +# network examples (comboat) +tinygo build -size short -o ./build/test.hex -target=elecrow-rp2040 -stack-size 8kb ./examples/net/tlsclient/ +tinygo build -size short -o ./build/test.hex -target=elecrow-rp2350 -stack-size 8kb ./examples/net/ntpclient/ From 382e79a748fa2646193758db88aad97296b524fb Mon Sep 17 00:00:00 2001 From: henryleduc <55436491+henryleduc@users.noreply.github.com> Date: Mon, 10 Feb 2025 18:13:58 +0000 Subject: [PATCH 060/137] fix(dhcp): fix typo in DHCP error message --- netdev/netdev.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netdev/netdev.go b/netdev/netdev.go index 52cf529..8090d02 100644 --- a/netdev/netdev.go +++ b/netdev/netdev.go @@ -35,8 +35,8 @@ var ( var ( ErrFamilyNotSupported = errors.New("Address family not supported") ErrProtocolNotSupported = errors.New("Socket protocol/type not supported") - ErrStartingDHCPClient = errors.New("Error starting DHPC client") - ErrStartingDHCPServer = errors.New("Error starting DHPC server") + ErrStartingDHCPClient = errors.New("Error starting DHCP client") + ErrStartingDHCPServer = errors.New("Error starting DHCP server") ErrNoMoreSockets = errors.New("No more sockets") ErrClosingSocket = errors.New("Error closing socket") ErrNotSupported = errors.New("Not supported") From afb1ea39c2fdf05bb0f41bda7803c06346a721de Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 4 Mar 2025 18:34:22 +0100 Subject: [PATCH 061/137] license: update year Signed-off-by: deadprogram --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 40bc82b..c4652c1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018-2024 The TinyGo Authors. All rights reserved. +Copyright (c) 2018-2025 The TinyGo Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are From 6e04decf19fb0524490f1e05cc4ffffe70c4d42c Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 4 Mar 2025 18:35:05 +0100 Subject: [PATCH 062/137] Prepare for release 0.30 Signed-off-by: deadprogram --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ version.go | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb065a2..b520aed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +0.30.0 +--- +- **new devices** + - **comboat** + - Add wifi driver comboat for Elecrow W5 rp2040 and rp2350 devices (#741) + - **max6675** + - Add MAX6675 device + - **TMC2209** + - Added TMC2209 support (#727) + - **TMC5160** + - Added TMC5160 support (#725) + - **sharpmem** + - Add sharpmem (#724) + +- **enhancements** + - **net** + - move to latest golang.org/x/net v0.33.0 (#732) + - **microphone** + - update microphone driver to use latest i2s interface + +- **bugfixes** + - **net** + - fix typo in DHCP error message + - **aht20** + - Fixed bug in aht20 driver + - **hub75** + - fix data buffering + + 0.29.0 --- - **new devices** diff --git a/version.go b/version.go index da56caa..ab14080 100644 --- a/version.go +++ b/version.go @@ -2,4 +2,4 @@ package drivers // Version returns a user-readable string showing the version of the drivers package for support purposes. // Update this value before release of new version of software. -const Version = "0.29.0" +const Version = "0.30.0" From 156d6e7c9ce473be185e1554c9da3c2983e6e26a Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 11 Mar 2025 18:27:15 +0100 Subject: [PATCH 063/137] chore: update all SPI usage to use either *machine.SPI or drivers.SPI to accomodate recent switch in machine package Signed-off-by: deadprogram --- examples/flash/console/spi/main.go | 2 +- examples/sdcard/console/feather-m4.go | 2 +- examples/sdcard/console/grandcentral-m4.go | 2 +- examples/sdcard/console/m0.go | 2 +- examples/sdcard/console/p1am-100.go | 2 +- examples/sdcard/console/pygamer.go | 2 +- examples/sdcard/console/pyportal.go | 2 +- examples/sdcard/console/wioterminal.go | 2 +- examples/tmc5160/main.go | 2 +- ili9341/spi_atsamd21.go | 4 ++-- ili9341/spi_atsamd51.go | 4 ++-- max6675/max6675.go | 6 ++++-- max72xx/max72xx.go | 6 ++++-- p1am/p1am.go | 3 ++- sx127x/sx127x.go | 2 +- tmc5160/README.MD | 2 +- tmc5160/spicomm.go | 12 ++++++------ waveshare-epd/epd1in54/epd1in54.go | 4 ++-- 18 files changed, 33 insertions(+), 28 deletions(-) diff --git a/examples/flash/console/spi/main.go b/examples/flash/console/spi/main.go index 21e088d..5d28fa7 100644 --- a/examples/flash/console/spi/main.go +++ b/examples/flash/console/spi/main.go @@ -10,7 +10,7 @@ import ( func main() { console_example.RunFor( flash.NewSPI( - &machine.SPI1, + machine.SPI1, machine.SPI1_SDO_PIN, machine.SPI1_SDI_PIN, machine.SPI1_SCK_PIN, diff --git a/examples/sdcard/console/feather-m4.go b/examples/sdcard/console/feather-m4.go index 258aac5..dae1fb0 100644 --- a/examples/sdcard/console/feather-m4.go +++ b/examples/sdcard/console/feather-m4.go @@ -7,7 +7,7 @@ import ( ) func init() { - spi = &machine.SPI0 + spi = machine.SPI0 sckPin = machine.SPI0_SCK_PIN sdoPin = machine.SPI0_SDO_PIN sdiPin = machine.SPI0_SDI_PIN diff --git a/examples/sdcard/console/grandcentral-m4.go b/examples/sdcard/console/grandcentral-m4.go index 51eb5c2..ed13803 100644 --- a/examples/sdcard/console/grandcentral-m4.go +++ b/examples/sdcard/console/grandcentral-m4.go @@ -7,7 +7,7 @@ import ( ) func init() { - spi = &machine.SPI1 + spi = machine.SPI1 sckPin = machine.SDCARD_SCK_PIN sdoPin = machine.SDCARD_SDO_PIN sdiPin = machine.SDCARD_SDI_PIN diff --git a/examples/sdcard/console/m0.go b/examples/sdcard/console/m0.go index 5ba5391..fcb0b03 100644 --- a/examples/sdcard/console/m0.go +++ b/examples/sdcard/console/m0.go @@ -7,7 +7,7 @@ import ( ) func init() { - spi = &machine.SPI0 + spi = machine.SPI0 sckPin = machine.SPI0_SCK_PIN sdoPin = machine.SPI0_SDO_PIN sdiPin = machine.SPI0_SDI_PIN diff --git a/examples/sdcard/console/p1am-100.go b/examples/sdcard/console/p1am-100.go index b1acb62..b003042 100644 --- a/examples/sdcard/console/p1am-100.go +++ b/examples/sdcard/console/p1am-100.go @@ -7,7 +7,7 @@ import ( ) func init() { - spi = &machine.SDCARD_SPI + spi = machine.SDCARD_SPI sckPin = machine.SDCARD_SCK_PIN sdoPin = machine.SDCARD_SDO_PIN sdiPin = machine.SDCARD_SDI_PIN diff --git a/examples/sdcard/console/pygamer.go b/examples/sdcard/console/pygamer.go index 3518244..cc1187c 100644 --- a/examples/sdcard/console/pygamer.go +++ b/examples/sdcard/console/pygamer.go @@ -7,7 +7,7 @@ import ( ) func init() { - spi = &machine.SPI0 + spi = machine.SPI0 sckPin = machine.SPI0_SCK_PIN sdoPin = machine.SPI0_SDO_PIN sdiPin = machine.SPI0_SDI_PIN diff --git a/examples/sdcard/console/pyportal.go b/examples/sdcard/console/pyportal.go index 9a8accb..3599cdf 100644 --- a/examples/sdcard/console/pyportal.go +++ b/examples/sdcard/console/pyportal.go @@ -7,7 +7,7 @@ import ( ) func init() { - spi = &machine.SPI0 + spi = machine.SPI0 sckPin = machine.SPI0_SCK_PIN sdoPin = machine.SPI0_SDO_PIN sdiPin = machine.SPI0_SDI_PIN diff --git a/examples/sdcard/console/wioterminal.go b/examples/sdcard/console/wioterminal.go index 96019e3..934b87a 100644 --- a/examples/sdcard/console/wioterminal.go +++ b/examples/sdcard/console/wioterminal.go @@ -7,7 +7,7 @@ import ( ) func init() { - spi = &machine.SPI2 + spi = machine.SPI2 sckPin = machine.SCK2 sdoPin = machine.SDO2 sdiPin = machine.SDI2 diff --git a/examples/tmc5160/main.go b/examples/tmc5160/main.go index 309ea14..3b60ccf 100644 --- a/examples/tmc5160/main.go +++ b/examples/tmc5160/main.go @@ -27,7 +27,7 @@ func main() { //bind csPin to driverAdddress driverAddress := uint8(0) // Let's assume we are working with driver at address 0x01 // Step 3. Bind the communication interface to the protocol - comm := tmc5160.NewSPIComm(*spi, csPins) + comm := tmc5160.NewSPIComm(spi, csPins) // Step 4. Define your stepper like this below //stepper := tmc5160.NewStepper(angle , gearRatio vSupply rCoil , lCoil , iPeak , rSense , mSteps, fclk ) stepper := tmc5160.NewDefaultStepper() // Default Stepper should be used only for testing. diff --git a/ili9341/spi_atsamd21.go b/ili9341/spi_atsamd21.go index 5aff3a9..c9582e4 100644 --- a/ili9341/spi_atsamd21.go +++ b/ili9341/spi_atsamd21.go @@ -8,10 +8,10 @@ import ( ) type spiDriver struct { - bus machine.SPI + bus *machine.SPI } -func NewSPI(bus machine.SPI, dc, cs, rst machine.Pin) *Device { +func NewSPI(bus *machine.SPI, dc, cs, rst machine.Pin) *Device { return &Device{ dc: dc, cs: cs, diff --git a/ili9341/spi_atsamd51.go b/ili9341/spi_atsamd51.go index 40b9995..a5cee85 100644 --- a/ili9341/spi_atsamd51.go +++ b/ili9341/spi_atsamd51.go @@ -8,10 +8,10 @@ import ( ) type spiDriver struct { - bus machine.SPI + bus *machine.SPI } -func NewSPI(bus machine.SPI, dc, cs, rst machine.Pin) *Device { +func NewSPI(bus *machine.SPI, dc, cs, rst machine.Pin) *Device { return &Device{ dc: dc, cs: cs, diff --git a/max6675/max6675.go b/max6675/max6675.go index 9d2433e..eb67596 100644 --- a/max6675/max6675.go +++ b/max6675/max6675.go @@ -4,6 +4,8 @@ package max6675 import ( "errors" "machine" + + "tinygo.org/x/drivers" ) // ErrThermocoupleOpen is returned when the thermocouple input is open. @@ -11,14 +13,14 @@ import ( var ErrThermocoupleOpen = errors.New("thermocouple input open") type Device struct { - bus machine.SPI + bus drivers.SPI cs machine.Pin } // Create a new Device to read from a MAX6675 thermocouple. // Pins must be configured before use. Frequency for SPI // should be 4.3MHz maximum. -func NewDevice(bus machine.SPI, cs machine.Pin) *Device { +func NewDevice(bus drivers.SPI, cs machine.Pin) *Device { return &Device{ bus: bus, cs: cs, diff --git a/max72xx/max72xx.go b/max72xx/max72xx.go index 27de1b5..ca6c819 100644 --- a/max72xx/max72xx.go +++ b/max72xx/max72xx.go @@ -4,17 +4,19 @@ package max72xx import ( "machine" + + "tinygo.org/x/drivers" ) type Device struct { - bus machine.SPI + bus drivers.SPI cs machine.Pin } // NewDriver creates a new max7219 connection. The SPI wire must already be configured // The SPI frequency must not be higher than 10MHz. // parameter cs: the datasheet also refers to this pin as "load" pin. -func NewDevice(bus machine.SPI, cs machine.Pin) *Device { +func NewDevice(bus drivers.SPI, cs machine.Pin) *Device { return &Device{ bus: bus, cs: cs, diff --git a/p1am/p1am.go b/p1am/p1am.go index d61817d..a46cd71 100644 --- a/p1am/p1am.go +++ b/p1am/p1am.go @@ -14,7 +14,8 @@ import ( ) type P1AM struct { - bus machine.SPI + bus *machine.SPI + slaveSelectPin, slaveAckPin, baseEnablePin machine.Pin // SkipAutoConfig will skip loading a default configuration into each module. diff --git a/sx127x/sx127x.go b/sx127x/sx127x.go index bced11a..f2c2a87 100644 --- a/sx127x/sx127x.go +++ b/sx127x/sx127x.go @@ -43,7 +43,7 @@ func (d *Device) GetRadioEventChan() chan lora.RadioEvent { } // New creates a new SX127x connection. The SPI bus must already be configured. -func New(spi machine.SPI, rstPin machine.Pin) *Device { +func New(spi drivers.SPI, rstPin machine.Pin) *Device { k := Device{ spi: spi, rstPin: rstPin, diff --git a/tmc5160/README.MD b/tmc5160/README.MD index b0b4916..4c4e11c 100644 --- a/tmc5160/README.MD +++ b/tmc5160/README.MD @@ -183,7 +183,7 @@ if err != nil { ## API Reference - NewSPIComm(spi machine.SPI, csPins map[uint8]machine.Pin) *SPIComm + NewSPIComm(spi *machine.SPI, csPins map[uint8]machine.Pin) *SPIComm Creates a new SPI communication interface for the TMC5160. diff --git a/tmc5160/spicomm.go b/tmc5160/spicomm.go index 4127f0f..fb3a46f 100644 --- a/tmc5160/spicomm.go +++ b/tmc5160/spicomm.go @@ -16,12 +16,12 @@ func (e CustomError) Error() string { // SPIComm implements RegisterComm for SPI-based communication type SPIComm struct { - spi machine.SPI + spi *machine.SPI CsPins map[uint8]machine.Pin // Map to store CS pin for each Driver by its address } // NewSPIComm creates a new SPIComm instance. -func NewSPIComm(spi machine.SPI, csPins map[uint8]machine.Pin) *SPIComm { +func NewSPIComm(spi *machine.SPI, csPins map[uint8]machine.Pin) *SPIComm { return &SPIComm{ spi: spi, CsPins: csPins, @@ -31,7 +31,7 @@ func NewSPIComm(spi machine.SPI, csPins map[uint8]machine.Pin) *SPIComm { // Setup initializes the SPI communication with the Driver and configures all CS pins. func (comm *SPIComm) Setup() error { // Check if SPI is initialized - if comm.spi == (machine.SPI{}) { + if comm.spi == nil { return CustomError("SPI not initialized") } @@ -66,7 +66,7 @@ func (comm *SPIComm) WriteRegister(register uint8, value uint32, driverAddress u addressWithWriteAccess := register | 0x80 // Send the address and the data to write (split into 4 bytes) - _, err := spiTransfer40(&comm.spi, addressWithWriteAccess, value) + _, err := spiTransfer40(comm.spi, addressWithWriteAccess, value) if err != nil { csPin.High() return CustomError("Failed to write register") @@ -88,7 +88,7 @@ func (comm *SPIComm) ReadRegister(register uint8, driverAddress uint8) (uint32, csPin.Low() // Step 1: Send a dummy write operation to begin the read sequence - _, err := spiTransfer40(&comm.spi, register, 0x00) // Send dummy data + _, err := spiTransfer40(comm.spi, register, 0x00) // Send dummy data if err != nil { csPin.High() return 0, CustomError("Failed to send dummy write") @@ -97,7 +97,7 @@ func (comm *SPIComm) ReadRegister(register uint8, driverAddress uint8) (uint32, time.Sleep(176 * time.Nanosecond) csPin.Low() // Step 2: Send the register read request again to get the actual value - response, err := spiTransfer40(&comm.spi, register, 0x00) // Send again to get actual register data + response, err := spiTransfer40(comm.spi, register, 0x00) // Send again to get actual register data if err != nil { csPin.High() return 0, CustomError("Failed to read register") diff --git a/waveshare-epd/epd1in54/epd1in54.go b/waveshare-epd/epd1in54/epd1in54.go index 4cbda2a..120b356 100644 --- a/waveshare-epd/epd1in54/epd1in54.go +++ b/waveshare-epd/epd1in54/epd1in54.go @@ -22,7 +22,7 @@ type Config struct { } type Device struct { - bus machine.SPI + bus *machine.SPI cs machine.Pin dc machine.Pin rst machine.Pin @@ -79,7 +79,7 @@ var partialRefresh = [159]uint8{ } // New returns a new epd1in54 driver. Pass in a fully configured SPI bus. -func New(bus machine.SPI, csPin, dcPin, rstPin, busyPin machine.Pin) Device { +func New(bus *machine.SPI, csPin, dcPin, rstPin, busyPin machine.Pin) Device { return Device{ buffer: make([]uint8, (uint32(Width)*uint32(Height))/8), bus: bus, From e7f90166ad330225232a8878d4594d9634dcb3d6 Mon Sep 17 00:00:00 2001 From: Craig Swank Date: Tue, 20 May 2025 04:27:30 -0600 Subject: [PATCH 064/137] tmc2209 bug fixes (#755) * Make write buffer big enough for crc * crc according to datasheet * fix build * a correct crc func already exists --- tmc2209/uartcomm.go | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/tmc2209/uartcomm.go b/tmc2209/uartcomm.go index e99947f..f1afb86 100644 --- a/tmc2209/uartcomm.go +++ b/tmc2209/uartcomm.go @@ -57,14 +57,10 @@ func (comm *UARTComm) WriteRegister(register uint8, value uint32, driverIndex ui byte((value >> 16) & 0xFF), // Middle byte byte((value >> 8) & 0xFF), // Next byte byte(value & 0xFF), // LSB of value + 0, // CRC } - // Calculate checksum by XORing all bytes - checksum := byte(0) - for _, b := range buffer[:7] { - checksum ^= b - } - buffer[7] = checksum // Set checksum byte + buffer[7] = CalculateCRC(buffer[:7]) // Write the data to the TMC2209 done := make(chan error, 1) @@ -86,10 +82,10 @@ func (comm *UARTComm) WriteRegister(register uint8, value uint32, driverIndex ui // ReadRegister sends a register read command to the TMC2209 with a timeout. func (comm *UARTComm) ReadRegister(register uint8, driverIndex uint8) (uint32, error) { var writeBuffer [4]byte - writeBuffer[0] = 0x05 // Sync byte - writeBuffer[1] = 0x00 // Slave address - writeBuffer[2] = register & 0x7F // Read command (MSB clear for read) - writeBuffer[3] = writeBuffer[0] ^ writeBuffer[1] ^ writeBuffer[2] // Checksum + writeBuffer[0] = 0x05 // Sync byte + writeBuffer[1] = 0x00 // Slave address + writeBuffer[2] = register & 0x7F // Read command (MSB clear for read) + writeBuffer[3] = CalculateCRC(writeBuffer[:3]) // Send the read command done := make(chan []byte, 1) @@ -103,11 +99,7 @@ func (comm *UARTComm) ReadRegister(register uint8, driverIndex uint8) (uint32, e // Implementing timeout using a 100ms timer select { case readBuffer := <-done: - // Validate checksum - checksum := byte(0) - for i := 0; i < 7; i++ { - checksum ^= readBuffer[i] - } + checksum := CalculateCRC(readBuffer[:7]) if checksum != readBuffer[7] { return 0, CustomError("checksum error") } From d41bc0b85f632d6cf8f5f96cd023af31a8ca31da Mon Sep 17 00:00:00 2001 From: Mateusz Nowak <118080320+mateusznowakdev@users.noreply.github.com> Date: Wed, 7 May 2025 00:06:12 +0200 Subject: [PATCH 065/137] fix: remove time.Sleep from SSD1306 SPI transfer code --- ssd1306/ssd1306.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/ssd1306/ssd1306.go b/ssd1306/ssd1306.go index e012918..dd8ebeb 100644 --- a/ssd1306/ssd1306.go +++ b/ssd1306/ssd1306.go @@ -322,7 +322,6 @@ func (b *SPIBus) tx(data []byte, isCommand bool) error { if isCommand { b.csPin.High() - time.Sleep(1 * time.Millisecond) b.dcPin.Low() b.csPin.Low() @@ -330,7 +329,6 @@ func (b *SPIBus) tx(data []byte, isCommand bool) error { b.csPin.High() } else { b.csPin.High() - time.Sleep(1 * time.Millisecond) b.dcPin.High() b.csPin.Low() From dbc9022f6abd13de635c9267e6ee7e38158ba6a4 Mon Sep 17 00:00:00 2001 From: Leon Matthews Date: Mon, 12 May 2025 09:35:12 +1200 Subject: [PATCH 066/137] ws2812: add 200MHz support for the Cortex-M0/rp2040 --- ws2812/ws2812-asm_cortexm.go | 469 +++++++++++++++++++++++++++++++++++ ws2812/ws2812.go | 2 +- ws2812/ws2812_cortexm.go | 5 +- 3 files changed, 474 insertions(+), 2 deletions(-) diff --git a/ws2812/ws2812-asm_cortexm.go b/ws2812/ws2812-asm_cortexm.go index 0cdece1..a13e7fa 100644 --- a/ws2812/ws2812-asm_cortexm.go +++ b/ws2812/ws2812-asm_cortexm.go @@ -1320,6 +1320,465 @@ void ws2812_writeByte168(char c, uint32_t *portSet, uint32_t *portClear, uint32_ [maskClear]"r"(maskClear), [portClear]"m"(*portClear)); } + +__attribute__((always_inline)) +void ws2812_writeByte200(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) { + // Timings: + // T0H: 70 - 72 cycles or 350.0ns - 360.0ns + // T1H: 210 - 212 cycles or 1050.0ns - 1060.0ns + // TLD: 230 - cycles or 1150.0ns - + uint32_t value = (uint32_t)c << 24; + char i = 8; + __asm__ __volatile__( + "1: @ send_bit\n" + "\t str %[maskSet], %[portSet] @ [2] T0H and T0L start here\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t lsls %[value], #1 @ [1]\n" + "\t bcs.n 2f @ [1/3] skip_store\n" + "\t str %[maskClear], %[portClear] @ [2] T0H -> T0L transition\n" + "\t2: @ skip_store\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t str %[maskClear], %[portClear] @ [2] T1H -> T1L transition\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t subs %[i], #1 @ [1]\n" + "\t beq.n 3f @ [1/3] end\n" + "\t b 1b @ [1/3] send_bit\n" + "\t3: @ end\n" + : [value]"+r"(value), + [i]"+r"(i) + : [maskSet]"r"(maskSet), + [portSet]"m"(*portSet), + [maskClear]"r"(maskClear), + [portClear]"m"(*portClear)); +} */ import "C" @@ -1382,3 +1841,13 @@ func (d Device) writeByte168(c byte) { interrupt.Restore(mask) } + +func (d Device) writeByte200(c byte) { + portSet, maskSet := d.Pin.PortMaskSet() + portClear, maskClear := d.Pin.PortMaskClear() + + mask := interrupt.Disable() + C.ws2812_writeByte200(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear)) + + interrupt.Restore(mask) +} diff --git a/ws2812/ws2812.go b/ws2812/ws2812.go index a0e0791..d5860f6 100644 --- a/ws2812/ws2812.go +++ b/ws2812/ws2812.go @@ -1,7 +1,7 @@ // Package ws2812 implements a driver for WS2812 and SK6812 RGB LED strips. package ws2812 // import "tinygo.org/x/drivers/ws2812" -//go:generate go run gen-ws2812.go -arch=cortexm 16 48 64 120 125 168 +//go:generate go run gen-ws2812.go -arch=cortexm 16 48 64 120 125 168 200 //go:generate go run gen-ws2812.go -arch=tinygoriscv 160 320 import ( diff --git a/ws2812/ws2812_cortexm.go b/ws2812/ws2812_cortexm.go index dafdb49..b818a67 100644 --- a/ws2812/ws2812_cortexm.go +++ b/ws2812/ws2812_cortexm.go @@ -28,12 +28,15 @@ func (d Device) WriteByte(c byte) error { case 120_000_000: // 120MHz d.writeByte120(c) return nil - case 125_000_000: // 125 MHz e.g. rp2040 + case 125_000_000: // 125 MHz e.g. rp2040 originally d.writeByte125(c) return nil case 168_000_000: // 168MHz, e.g. stm32f405 d.writeByte168(c) return nil + case 200_000_000: // 200MHz, e.g. rp2040 starting with TinyGo v0.37 + d.writeByte200(c) + return nil default: return errUnknownClockSpeed } From c4168864fd1c3896ad73747e2cf0d28ab0f37d3e Mon Sep 17 00:00:00 2001 From: Hikmatulloh Hari Mukti Date: Thu, 15 May 2025 15:25:02 +0700 Subject: [PATCH 067/137] bmp280: remove alloc on read sensor data --- bmp280/bmp280.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/bmp280/bmp280.go b/bmp280/bmp280.go index 2e769c1..e3b21e5 100644 --- a/bmp280/bmp280.go +++ b/bmp280/bmp280.go @@ -23,6 +23,7 @@ type Filter uint type Device struct { bus drivers.I2C Address uint16 + buf [6]byte cali calibrationCoefficients Temperature Oversampling Pressure Oversampling @@ -134,8 +135,8 @@ func (d *Device) PrintCali() { // ReadTemperature returns the temperature in celsius milli degrees (°C/1000). func (d *Device) ReadTemperature() (temperature int32, err error) { - data, err := d.readData(REG_TEMP, 3) - if err != nil { + data := d.buf[:3] + if err = d.readData(REG_TEMP, data); err != nil { return } @@ -158,8 +159,8 @@ func (d *Device) ReadTemperature() (temperature int32, err error) { // ReadPressure returns the pressure in milli pascals (mPa). func (d *Device) ReadPressure() (pressure int32, err error) { // First 3 bytes are Pressure, last 3 bytes are Temperature - data, err := d.readData(REG_PRES, 6) - if err != nil { + data := d.buf[:6] + if err = d.readData(REG_PRES, data); err != nil { return } @@ -203,7 +204,7 @@ func (d *Device) ReadPressure() (pressure int32, err error) { } // readData reads n number of bytes of the specified register -func (d *Device) readData(register int, n int) ([]byte, error) { +func (d *Device) readData(register int, data []byte) error { // If not in normal mode, set the mode to FORCED mode, to prevent incorrect measurements // After the measurement in FORCED mode, the sensor will return to SLEEP mode if d.Mode != MODE_NORMAL { @@ -218,9 +219,7 @@ func (d *Device) readData(register int, n int) ([]byte, error) { } // Read the requested register - data := make([]byte, n) - err := legacy.ReadRegister(d.bus, uint8(d.Address), uint8(register), data[:]) - return data, err + return legacy.ReadRegister(d.bus, uint8(d.Address), uint8(register), data[:]) } // convert3Bytes converts three bytes to int32 From 82c41dbf1430d00dfdcf103874cb296a035b2ea8 Mon Sep 17 00:00:00 2001 From: soypat Date: Tue, 8 Apr 2025 17:04:03 -0300 Subject: [PATCH 068/137] add driver design pointer to CONTRIBUTING.md --- CONTRIBUTING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d77648..10c7f8b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,6 +8,9 @@ We would like your help to make this project better, so we appreciate any contri We'd love to get your feedback on getting started with TinyGo. Run into any difficulty, confusion, or anything else? You are not alone. We want to know about your experience, so we can help the next people. Please open a Github issue with your questions, or you can also get in touch directly with us on our Slack channel at [https://gophers.slack.com/messages/CDJD3SUP6](https://gophers.slack.com/messages/CDJD3SUP6). +### Driver design +Before porting or writing a driver from scratch please read **[Driver Design for TinyGo](https://tinygo.org/docs/guides/driver-design)**. + ### One of the TinyGo drivers is not working as you expect Please open a Github issue with your problem, and we will be happy to assist. From c4ff8242a7726cb0d281d7c583d124e8d52bca00 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Fri, 13 Jun 2025 18:58:02 +0200 Subject: [PATCH 069/137] all: updates for drivers release v0.32.0 Signed-off-by: deadprogram --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ version.go | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b520aed..4e0229f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +0.32.0 +--- +- **enhancements** + - **bmp280** + - remove alloc on read sensor data + - **ws2812** + - add 200MHz support for the Cortex-M0/rp2040 + +- **bugfixes** + - **ssd1306** + - remove time.Sleep from SSD1306 SPI transfer code + - **tmc2209** + - tmc2209 bug fixes (#755) + +- **docs** + - **contributing** + - add driver design pointer to CONTRIBUTING.md + + +0.31.0 +--- +--- +- **enhancements** + - **spi** + - update all SPI usage to use either *machine.SPI or drivers.SPI + + 0.30.0 --- - **new devices** diff --git a/version.go b/version.go index ab14080..8468fe8 100644 --- a/version.go +++ b/version.go @@ -2,4 +2,4 @@ package drivers // Version returns a user-readable string showing the version of the drivers package for support purposes. // Update this value before release of new version of software. -const Version = "0.30.0" +const Version = "0.32.0" From 80356fd9d97d8ad39bf5c90a57357a65fb81e9d5 Mon Sep 17 00:00:00 2001 From: Patricio Whittingslow Date: Sun, 13 Jul 2025 09:58:49 -0300 Subject: [PATCH 070/137] add regmap package to facilitate heapless driver development (#768) --- internal/regmap/devices.go | 121 +++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 internal/regmap/devices.go diff --git a/internal/regmap/devices.go b/internal/regmap/devices.go new file mode 100644 index 0000000..348dc6a --- /dev/null +++ b/internal/regmap/devices.go @@ -0,0 +1,121 @@ +package regmap + +import ( + "encoding/binary" + "io" + + "tinygo.org/x/drivers" +) + +// Device8 implements common logic to most 8-bit peripherals with an I2C or SPI bus. +type Device8 struct { + buf [10]byte +} + +// clear zeroes Device8's buffers. +func (d *Device8) clear() { + d.buf = [10]byte{} +} + +// I2C methods. + +func (d *Device8) Read8I2C(bus drivers.I2C, i2cAddr uint16, addr uint8) (byte, error) { + d.buf[0] = addr + err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:2]) + return d.buf[1], err +} + +func (d *Device8) Read16I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, order binary.ByteOrder) (uint16, error) { + d.buf[0] = addr + err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:3]) + return order.Uint16(d.buf[1:3]), err +} + +func (d *Device8) Read32I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, order binary.ByteOrder) (uint32, error) { + d.buf[0] = addr + err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:5]) + return order.Uint32(d.buf[1:5]), err +} + +func (d *Device8) ReadDataI2C(bus drivers.I2C, i2cAddr uint16, addr uint8, dataDestination []byte) error { + d.buf[0] = addr + return bus.Tx(i2cAddr, d.buf[:1], dataDestination) +} + +func (d *Device8) Write8I2C(bus drivers.I2C, i2cAddr uint16, addr, value uint8) error { + d.buf[0] = addr + d.buf[1] = value + return bus.Tx(i2cAddr, d.buf[:2], nil) +} + +func (d *Device8) Write16I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, value uint16, order binary.ByteOrder) error { + d.buf[0] = addr + order.PutUint16(d.buf[1:3], value) + return bus.Tx(i2cAddr, d.buf[0:3], nil) +} + +func (d *Device8) Write32I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, value uint32, order binary.ByteOrder) error { + d.buf[0] = addr + order.PutUint32(d.buf[1:5], value) + return bus.Tx(i2cAddr, d.buf[0:5], nil) +} + +// SPI methods. + +func (d *Device8) Read8SPI(bus drivers.SPI, addr uint8) (byte, error) { + d.clear() + d.buf[0] = addr + err := bus.Tx(d.buf[0:1], d.buf[1:2]) // We suppose data is returned after first byte in SPI. + return d.buf[1], err +} + +func (d *Device8) Read16SPI(bus drivers.SPI, addr uint8, order binary.ByteOrder) (uint16, error) { + d.clear() + d.buf[0] = addr + err := bus.Tx(d.buf[0:3], d.buf[3:6]) // We suppose data is returned after first byte in SPI. + return order.Uint16(d.buf[4:6]), err +} + +func (d *Device8) Read32SPI(bus drivers.SPI, addr uint8, order binary.ByteOrder) (uint32, error) { + d.clear() + d.buf[0] = addr + err := bus.Tx(d.buf[0:5], d.buf[5:10]) // We suppose data is returned after first byte in SPI. + return order.Uint32(d.buf[6:10]), err +} + +// ReadDataSPI reads data from a 8bit device address. It assumes data at register address is sent back +// from device after first byte is written as address. +// It needs the auxiliary buffer length to be large enough to contain both the write and read portions of buffer, +// so 2*(dataLength+1) < len(auxiliaryBuf) must hold. +func (d *Device8) ReadDataSPI(bus drivers.SPI, addr uint8, dataLength int, auxiliaryBuf []byte) ([]byte, error) { + split := len(auxiliaryBuf) / 2 + if split < dataLength+1 { + return nil, io.ErrShortBuffer + } + + wbuf, rbuf := auxiliaryBuf[:split], auxiliaryBuf[split:] + wbuf[0] = addr + err := bus.Tx(wbuf, rbuf) + return rbuf[1:], err +} + +func (d *Device8) Write8SPI(bus drivers.SPI, addr, value uint8) error { + d.clear() + d.buf[0] = addr + d.buf[1] = value + return bus.Tx(d.buf[:2], nil) +} + +func (d *Device8) Write16SPI(bus drivers.SPI, addr uint8, value uint16, order binary.ByteOrder) error { + d.clear() + d.buf[0] = addr + order.PutUint16(d.buf[1:3], value) + return bus.Tx(d.buf[:3], nil) +} + +func (d *Device8) Write32SPI(bus drivers.SPI, addr uint8, value uint32, order binary.ByteOrder) error { + d.clear() + d.buf[0] = addr + order.PutUint32(d.buf[1:5], value) + return bus.Tx(d.buf[:5], nil) +} From 7de0a0814eed63863700d3ea4891bc6a655189fd Mon Sep 17 00:00:00 2001 From: Ron Evans Date: Mon, 14 Jul 2025 16:16:56 +0200 Subject: [PATCH 071/137] Revert "add regmap package to facilitate heapless driver development (#768)" (#776) This reverts commit 80356fd9d97d8ad39bf5c90a57357a65fb81e9d5. --- internal/regmap/devices.go | 121 ------------------------------------- 1 file changed, 121 deletions(-) delete mode 100644 internal/regmap/devices.go diff --git a/internal/regmap/devices.go b/internal/regmap/devices.go deleted file mode 100644 index 348dc6a..0000000 --- a/internal/regmap/devices.go +++ /dev/null @@ -1,121 +0,0 @@ -package regmap - -import ( - "encoding/binary" - "io" - - "tinygo.org/x/drivers" -) - -// Device8 implements common logic to most 8-bit peripherals with an I2C or SPI bus. -type Device8 struct { - buf [10]byte -} - -// clear zeroes Device8's buffers. -func (d *Device8) clear() { - d.buf = [10]byte{} -} - -// I2C methods. - -func (d *Device8) Read8I2C(bus drivers.I2C, i2cAddr uint16, addr uint8) (byte, error) { - d.buf[0] = addr - err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:2]) - return d.buf[1], err -} - -func (d *Device8) Read16I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, order binary.ByteOrder) (uint16, error) { - d.buf[0] = addr - err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:3]) - return order.Uint16(d.buf[1:3]), err -} - -func (d *Device8) Read32I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, order binary.ByteOrder) (uint32, error) { - d.buf[0] = addr - err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:5]) - return order.Uint32(d.buf[1:5]), err -} - -func (d *Device8) ReadDataI2C(bus drivers.I2C, i2cAddr uint16, addr uint8, dataDestination []byte) error { - d.buf[0] = addr - return bus.Tx(i2cAddr, d.buf[:1], dataDestination) -} - -func (d *Device8) Write8I2C(bus drivers.I2C, i2cAddr uint16, addr, value uint8) error { - d.buf[0] = addr - d.buf[1] = value - return bus.Tx(i2cAddr, d.buf[:2], nil) -} - -func (d *Device8) Write16I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, value uint16, order binary.ByteOrder) error { - d.buf[0] = addr - order.PutUint16(d.buf[1:3], value) - return bus.Tx(i2cAddr, d.buf[0:3], nil) -} - -func (d *Device8) Write32I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, value uint32, order binary.ByteOrder) error { - d.buf[0] = addr - order.PutUint32(d.buf[1:5], value) - return bus.Tx(i2cAddr, d.buf[0:5], nil) -} - -// SPI methods. - -func (d *Device8) Read8SPI(bus drivers.SPI, addr uint8) (byte, error) { - d.clear() - d.buf[0] = addr - err := bus.Tx(d.buf[0:1], d.buf[1:2]) // We suppose data is returned after first byte in SPI. - return d.buf[1], err -} - -func (d *Device8) Read16SPI(bus drivers.SPI, addr uint8, order binary.ByteOrder) (uint16, error) { - d.clear() - d.buf[0] = addr - err := bus.Tx(d.buf[0:3], d.buf[3:6]) // We suppose data is returned after first byte in SPI. - return order.Uint16(d.buf[4:6]), err -} - -func (d *Device8) Read32SPI(bus drivers.SPI, addr uint8, order binary.ByteOrder) (uint32, error) { - d.clear() - d.buf[0] = addr - err := bus.Tx(d.buf[0:5], d.buf[5:10]) // We suppose data is returned after first byte in SPI. - return order.Uint32(d.buf[6:10]), err -} - -// ReadDataSPI reads data from a 8bit device address. It assumes data at register address is sent back -// from device after first byte is written as address. -// It needs the auxiliary buffer length to be large enough to contain both the write and read portions of buffer, -// so 2*(dataLength+1) < len(auxiliaryBuf) must hold. -func (d *Device8) ReadDataSPI(bus drivers.SPI, addr uint8, dataLength int, auxiliaryBuf []byte) ([]byte, error) { - split := len(auxiliaryBuf) / 2 - if split < dataLength+1 { - return nil, io.ErrShortBuffer - } - - wbuf, rbuf := auxiliaryBuf[:split], auxiliaryBuf[split:] - wbuf[0] = addr - err := bus.Tx(wbuf, rbuf) - return rbuf[1:], err -} - -func (d *Device8) Write8SPI(bus drivers.SPI, addr, value uint8) error { - d.clear() - d.buf[0] = addr - d.buf[1] = value - return bus.Tx(d.buf[:2], nil) -} - -func (d *Device8) Write16SPI(bus drivers.SPI, addr uint8, value uint16, order binary.ByteOrder) error { - d.clear() - d.buf[0] = addr - order.PutUint16(d.buf[1:3], value) - return bus.Tx(d.buf[:3], nil) -} - -func (d *Device8) Write32SPI(bus drivers.SPI, addr uint8, value uint32, order binary.ByteOrder) error { - d.clear() - d.buf[0] = addr - order.PutUint32(d.buf[1:5], value) - return bus.Tx(d.buf[:5], nil) -} From 0304d30b786b29adf144c6666651bd3730bf7c70 Mon Sep 17 00:00:00 2001 From: Yurii Soldak Date: Wed, 25 Jun 2025 10:03:41 +0200 Subject: [PATCH 072/137] lsm6ds3tr: avoid unnecessary heap allocations (#766) * lsm6ds3tr: avoid unnecessary heap allocations * lsm6ds3tr: use helper functions, for readability * lsm6ds3tr: return slice of the internal buffer on readBytes --- lsm6ds3tr/lsm6ds3tr.go | 59 +++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/lsm6ds3tr/lsm6ds3tr.go b/lsm6ds3tr/lsm6ds3tr.go index b8be8d8..76b91d1 100644 --- a/lsm6ds3tr/lsm6ds3tr.go +++ b/lsm6ds3tr/lsm6ds3tr.go @@ -8,7 +8,6 @@ import ( "errors" "tinygo.org/x/drivers" - "tinygo.org/x/drivers/internal/legacy" ) type AccelRange uint8 @@ -26,7 +25,7 @@ type Device struct { accelSampleRate AccelSampleRate gyroRange GyroRange gyroSampleRate GyroSampleRate - buf [6]uint8 + buf [7]uint8 // up to 6 bytes for read + 1 byte for the register address } // Configuration for LSM6DS3TR device. @@ -84,30 +83,20 @@ func (d *Device) doConfigure(cfg Configuration) (err error) { d.gyroSampleRate = GYRO_SR_104 } - data := d.buf[:1] - // Configure accelerometer - data[0] = uint8(d.accelRange) | uint8(d.accelSampleRate) - err = legacy.WriteRegister(d.bus, uint8(d.Address), CTRL1_XL, data) + err = d.writeByte(CTRL1_XL, uint8(d.accelRange)|uint8(d.accelSampleRate)) if err != nil { return } - // Set ODR bit - err = legacy.ReadRegister(d.bus, uint8(d.Address), CTRL4_C, data) - if err != nil { - return - } - data[0] = data[0] &^ BW_SCAL_ODR_ENABLED - data[0] |= BW_SCAL_ODR_ENABLED - err = legacy.WriteRegister(d.bus, uint8(d.Address), CTRL4_C, data) + // Enable ODR scaling + err = d.setBits(CTRL4_C, BW_SCAL_ODR_ENABLED) if err != nil { return } // Configure gyroscope - data[0] = uint8(d.gyroRange) | uint8(d.gyroSampleRate) - err = legacy.WriteRegister(d.bus, uint8(d.Address), CTRL2_G, data) + err = d.writeByte(CTRL2_G, uint8(d.gyroRange)|uint8(d.gyroSampleRate)) if err != nil { return } @@ -118,8 +107,10 @@ func (d *Device) doConfigure(cfg Configuration) (err error) { // Connected returns whether a LSM6DS3TR has been found. // It does a "who am I" request and checks the response. func (d *Device) Connected() bool { - data := d.buf[:1] - legacy.ReadRegister(d.bus, uint8(d.Address), WHO_AM_I, data) + data, err := d.readBytes(WHO_AM_I, 1) + if err != nil { + return false + } return data[0] == 0x6A } @@ -128,8 +119,7 @@ func (d *Device) Connected() bool { // and the sensor is not moving the returned value will be around 1000000 or // -1000000. func (d *Device) ReadAcceleration() (x, y, z int32, err error) { - data := d.buf[:6] - err = legacy.ReadRegister(d.bus, uint8(d.Address), OUTX_L_XL, data) + data, err := d.readBytes(OUTX_L_XL, 6) if err != nil { return } @@ -153,8 +143,7 @@ func (d *Device) ReadAcceleration() (x, y, z int32, err error) { // rotation along one axis and while doing so integrate all values over time, // you would get a value close to 360000000. func (d *Device) ReadRotation() (x, y, z int32, err error) { - data := d.buf[:6] - err = legacy.ReadRegister(d.bus, uint8(d.Address), OUTX_L_G, data) + data, err := d.readBytes(OUTX_L_G, 6) if err != nil { return } @@ -177,8 +166,7 @@ func (d *Device) ReadRotation() (x, y, z int32, err error) { // ReadTemperature returns the temperature in celsius milli degrees (°C/1000) func (d *Device) ReadTemperature() (t int32, err error) { - data := d.buf[:2] - err = legacy.ReadRegister(d.bus, uint8(d.Address), OUT_TEMP_L, data) + data, err := d.readBytes(OUT_TEMP_L, 2) if err != nil { return } @@ -187,3 +175,26 @@ func (d *Device) ReadTemperature() (t int32, err error) { t = 25000 + (int32(int16((int16(data[1])<<8)|int16(data[0])))*125)/32 return } + +func (d *Device) readBytes(reg, size uint8) ([]byte, error) { + d.buf[0] = reg + err := d.bus.Tx(d.Address, d.buf[0:1], d.buf[1:size+1]) + if err != nil { + return nil, err + } + return d.buf[1 : size+1], nil +} + +func (d *Device) writeByte(reg, value uint8) error { + d.buf[0] = reg + d.buf[1] = value + return d.bus.Tx(d.Address, d.buf[0:2], nil) +} + +func (d *Device) setBits(reg, bits uint8) error { + data, err := d.readBytes(reg, 1) + if err != nil { + return err + } + return d.writeByte(reg, (data[0]&^bits)|bits) +} From 45fad80c3e6f6c60ebed5e0676df4b530aea05e7 Mon Sep 17 00:00:00 2001 From: JP Hastings-Spital Date: Mon, 30 Jun 2025 08:16:15 +0100 Subject: [PATCH 073/137] feat: allow gps init with address Adafruit's Mini GPS PA1010D Module works with this device driver, but requires 0x10 as the address, rather than 0x42. This change allows the device to be initialised with whatever i2c address is needed, while maintaining backward compatibility. Adds new constants to allow easy configuration of both the ublox device and the PA1010D. --- examples/gps/i2c/main.go | 2 +- gps/gps.go | 8 +++++++- gps/registers.go | 6 +++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/examples/gps/i2c/main.go b/examples/gps/i2c/main.go index 8f7873c..123cc26 100644 --- a/examples/gps/i2c/main.go +++ b/examples/gps/i2c/main.go @@ -10,7 +10,7 @@ import ( func main() { println("GPS I2C Example") machine.I2C0.Configure(machine.I2CConfig{}) - ublox := gps.NewI2C(machine.I2C0) + ublox := gps.NewI2CWithAddress(machine.I2C0, gps.UBLOX_I2C_ADDRESS) parser := gps.NewParser() var fix gps.Fix for { diff --git a/gps/gps.go b/gps/gps.go index 86bfd6a..16ba870 100644 --- a/gps/gps.go +++ b/gps/gps.go @@ -69,10 +69,16 @@ func NewUART(uart drivers.UART) Device { } // NewI2C creates a new I2C GPS connection. +// Uses the default i2c address (0x42) for backward compatibility reasons. func NewI2C(bus drivers.I2C) Device { + return NewI2CWithAddress(bus, I2C_ADDRESS) +} + +// NewI2CWithAddress creates a new I2C GPS connection on the provided address +func NewI2CWithAddress(bus drivers.I2C, i2cAddress uint16) Device { return Device{ bus: bus, - address: I2C_ADDRESS, + address: i2cAddress, buffer: make([]byte, bufferSize), bufIdx: bufferSize, sentence: strings.Builder{}, diff --git a/gps/registers.go b/gps/registers.go index 5ef9918..6d74d87 100644 --- a/gps/registers.go +++ b/gps/registers.go @@ -4,7 +4,11 @@ package gps // The I2C address which this device listens to. const ( - I2C_ADDRESS = 0x42 + // To ensure backward compatibility + I2C_ADDRESS = UBLOX_I2C_ADDRESS + + UBLOX_I2C_ADDRESS = 0x42 + PA1010D_I2C_ADDRESS = 0x10 ) const ( From ae9e8f915e07503e94fb906ccb79fe92d6cb6a18 Mon Sep 17 00:00:00 2001 From: Yurii Soldak Date: Sun, 13 Jul 2025 14:53:58 +0200 Subject: [PATCH 074/137] ssd1306: avoid unnecessary heap allocations (#767) * ssd1306: avoid unnecessary heap allocations * ssd1306: extract i2c and spi bus implementations * ssd1306: refactor tests -- show fps and heap usage * ssd1306: bring back the lost exported methods * Adjust examples * Fix smoketests for ssd1306 --- examples/ssd1306/i2c_128x32/main.go | 51 ------- examples/ssd1306/i2c_128x64/main.go | 60 -------- examples/ssd1306/main.go | 59 ++++++++ examples/ssd1306/main_i2c_xiao-ble.go | 38 ++++++ examples/ssd1306/main_spi_thumby.go | 27 ++++ examples/ssd1306/main_spi_xiao-rp2040.go | 40 ++++++ examples/ssd1306/spi_128x64/main.go | 48 ------- examples/ssd1306/spi_thumby/main.go | 50 ------- smoketest.sh | 5 +- ssd1306/ssd1306.go | 166 ++++------------------- ssd1306/ssd1306_i2c.go | 52 +++++++ ssd1306/ssd1306_spi.go | 68 ++++++++++ 12 files changed, 316 insertions(+), 348 deletions(-) delete mode 100644 examples/ssd1306/i2c_128x32/main.go delete mode 100644 examples/ssd1306/i2c_128x64/main.go create mode 100644 examples/ssd1306/main.go create mode 100644 examples/ssd1306/main_i2c_xiao-ble.go create mode 100644 examples/ssd1306/main_spi_thumby.go create mode 100644 examples/ssd1306/main_spi_xiao-rp2040.go delete mode 100644 examples/ssd1306/spi_128x64/main.go delete mode 100644 examples/ssd1306/spi_thumby/main.go create mode 100644 ssd1306/ssd1306_i2c.go create mode 100644 ssd1306/ssd1306_spi.go diff --git a/examples/ssd1306/i2c_128x32/main.go b/examples/ssd1306/i2c_128x32/main.go deleted file mode 100644 index 8baf37d..0000000 --- a/examples/ssd1306/i2c_128x32/main.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "machine" - - "image/color" - "time" - - "tinygo.org/x/drivers/ssd1306" -) - -func main() { - machine.I2C0.Configure(machine.I2CConfig{ - Frequency: machine.TWI_FREQ_400KHZ, - }) - - display := ssd1306.NewI2C(machine.I2C0) - display.Configure(ssd1306.Config{ - Address: ssd1306.Address_128_32, - Width: 128, - Height: 32, - }) - - display.ClearDisplay() - - x := int16(0) - y := int16(0) - deltaX := int16(1) - deltaY := int16(1) - for { - pixel := display.GetPixel(x, y) - c := color.RGBA{255, 255, 255, 255} - if pixel { - c = color.RGBA{0, 0, 0, 255} - } - display.SetPixel(x, y, c) - display.Display() - - x += deltaX - y += deltaY - - if x == 0 || x == 127 { - deltaX = -deltaX - } - - if y == 0 || y == 31 { - deltaY = -deltaY - } - time.Sleep(1 * time.Millisecond) - } -} diff --git a/examples/ssd1306/i2c_128x64/main.go b/examples/ssd1306/i2c_128x64/main.go deleted file mode 100644 index a17010d..0000000 --- a/examples/ssd1306/i2c_128x64/main.go +++ /dev/null @@ -1,60 +0,0 @@ -// This example shows how to use 128x64 display over I2C -// Tested on Seeeduino XIAO Expansion Board https://wiki.seeedstudio.com/Seeeduino-XIAO-Expansion-Board/ -// -// According to manual, I2C address of the display is 0x78, but that's 8-bit address. -// TinyGo operates on 7-bit addresses and respective 7-bit address would be 0x3C, which we use below. -// -// To learn more about different types of I2C addresses, please see following page -// https://www.totalphase.com/support/articles/200349176-7-bit-8-bit-and-10-bit-I2C-Slave-Addressing - -package main - -import ( - "machine" - - "image/color" - "time" - - "tinygo.org/x/drivers/ssd1306" -) - -func main() { - machine.I2C0.Configure(machine.I2CConfig{ - Frequency: machine.TWI_FREQ_400KHZ, - }) - - display := ssd1306.NewI2C(machine.I2C0) - display.Configure(ssd1306.Config{ - Address: 0x3C, - Width: 128, - Height: 64, - }) - - display.ClearDisplay() - - x := int16(0) - y := int16(0) - deltaX := int16(1) - deltaY := int16(1) - for { - pixel := display.GetPixel(x, y) - c := color.RGBA{255, 255, 255, 255} - if pixel { - c = color.RGBA{0, 0, 0, 255} - } - display.SetPixel(x, y, c) - display.Display() - - x += deltaX - y += deltaY - - if x == 0 || x == 127 { - deltaX = -deltaX - } - - if y == 0 || y == 63 { - deltaY = -deltaY - } - time.Sleep(1 * time.Millisecond) - } -} diff --git a/examples/ssd1306/main.go b/examples/ssd1306/main.go new file mode 100644 index 0000000..a7f412d --- /dev/null +++ b/examples/ssd1306/main.go @@ -0,0 +1,59 @@ +package main + +// This example shows how to use SSD1306 OLED display driver over I2C and SPI. +// +// Check the `newSSD1306Display()` functions for I2C and SPI initializations. + +import ( + "runtime" + + "image/color" + "time" +) + +func main() { + + display := newSSD1306Display() + display.ClearDisplay() + + w, h := display.Size() + x := int16(0) + y := int16(0) + deltaX := int16(1) + deltaY := int16(1) + + traceTime := time.Now().UnixMilli() + 1000 + frames := 0 + ms := runtime.MemStats{} + + for { + pixel := display.GetPixel(x, y) + c := color.RGBA{255, 255, 255, 255} + if pixel { + c = color.RGBA{0, 0, 0, 255} + } + display.SetPixel(x, y, c) + display.Display() + + x += deltaX + y += deltaY + + if x == 0 || x == w-1 { + deltaX = -deltaX + } + + if y == 0 || y == h-1 { + deltaY = -deltaY + } + + frames++ + now := time.Now().UnixMilli() + if now >= traceTime { + runtime.ReadMemStats(&ms) + println("TS", now, "| FPS", frames, "| HeapInuse", ms.HeapInuse) + traceTime = now + 1000 + frames = 0 + } + } + +} diff --git a/examples/ssd1306/main_i2c_xiao-ble.go b/examples/ssd1306/main_i2c_xiao-ble.go new file mode 100644 index 0000000..c074df2 --- /dev/null +++ b/examples/ssd1306/main_i2c_xiao-ble.go @@ -0,0 +1,38 @@ +//go:build xiao_ble + +// This initializes SSD1306 OLED display driver over I2C. +// +// Seeed XIAO BLE board + SSD1306 128x32 I2C OLED display. +// +// Wiring: +// - XIAO GND -> OLED GND +// - XIAO 3v3 -> OLED VCC +// - XIAO D4 (SDA) -> OLED SDA +// - XIAO D5 (SCL) -> OLED SCK +// +// For your case: +// - Connect the display to I2C pins on your board. +// - Adjust I2C address and display size as needed. + +package main + +import ( + "machine" + + "tinygo.org/x/drivers/ssd1306" +) + +func newSSD1306Display() *ssd1306.Device { + machine.I2C0.Configure(machine.I2CConfig{ + Frequency: 400 * machine.KHz, + SDA: machine.SDA0_PIN, + SCL: machine.SCL0_PIN, + }) + display := ssd1306.NewI2C(machine.I2C0) + display.Configure(ssd1306.Config{ + Address: ssd1306.Address_128_32, // or ssd1306.Address + Width: 128, + Height: 32, // or 64 + }) + return display +} diff --git a/examples/ssd1306/main_spi_thumby.go b/examples/ssd1306/main_spi_thumby.go new file mode 100644 index 0000000..f68164b --- /dev/null +++ b/examples/ssd1306/main_spi_thumby.go @@ -0,0 +1,27 @@ +//go:build thumby + +// This initializes SSD1306 OLED display driver over SPI. +// +// Thumby board has a tiny built-in 72x40 display. +// +// As the display is built-in, no wiring is needed. + +package main + +import ( + "machine" + + "tinygo.org/x/drivers/ssd1306" +) + +func newSSD1306Display() *ssd1306.Device { + machine.SPI0.Configure(machine.SPIConfig{}) + display := ssd1306.NewSPI(machine.SPI0, machine.THUMBY_DC_PIN, machine.THUMBY_RESET_PIN, machine.THUMBY_CS_PIN) + display.Configure(ssd1306.Config{ + Width: 72, + Height: 40, + ResetCol: ssd1306.ResetValue{28, 99}, + ResetPage: ssd1306.ResetValue{0, 5}, + }) + return display +} diff --git a/examples/ssd1306/main_spi_xiao-rp2040.go b/examples/ssd1306/main_spi_xiao-rp2040.go new file mode 100644 index 0000000..fd50bd4 --- /dev/null +++ b/examples/ssd1306/main_spi_xiao-rp2040.go @@ -0,0 +1,40 @@ +//go:build xiao_rp2040 + +// This initializes SSD1306 OLED display driver over SPI. +// +// Seeed XIAO RP2040 board + SSD1306 128x64 SPI OLED display. +// +// Wiring: +// - XIAO GND -> OLED GND +// - XIAO 3v3 -> OLED VCC +// - XIAO D8 (SCK) -> OLED D0 +// - XIAO D10 (SDO) -> OLED D1 +// - XIAO D4 -> OLED RES +// - XIAO D5 -> OLED DC +// - XIAO D6 -> OLED CS +// +// For your case: +// - Connect the display to SPI pins on your board. +// - Adjust RES, DC and CS pins as needed. +// - Adjust SPI frequency as needed. +// - Adjust display size as needed. + +package main + +import ( + "machine" + + "tinygo.org/x/drivers/ssd1306" +) + +func newSSD1306Display() *ssd1306.Device { + machine.SPI0.Configure(machine.SPIConfig{ + Frequency: 50 * machine.MHz, + }) + display := ssd1306.NewSPI(machine.SPI0, machine.D5, machine.D4, machine.D6) + display.Configure(ssd1306.Config{ + Width: 128, + Height: 64, + }) + return display +} diff --git a/examples/ssd1306/spi_128x64/main.go b/examples/ssd1306/spi_128x64/main.go deleted file mode 100644 index 094f5ca..0000000 --- a/examples/ssd1306/spi_128x64/main.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "image/color" - "machine" - "time" - - "tinygo.org/x/drivers/ssd1306" -) - -func main() { - machine.SPI0.Configure(machine.SPIConfig{ - Frequency: 8000000, - }) - display := ssd1306.NewSPI(machine.SPI0, machine.P8, machine.P7, machine.P9) - display.Configure(ssd1306.Config{ - Width: 128, - Height: 64, - }) - - display.ClearDisplay() - - x := int16(64) - y := int16(32) - deltaX := int16(1) - deltaY := int16(1) - for { - pixel := display.GetPixel(x, y) - c := color.RGBA{255, 255, 255, 255} - if pixel { - c = color.RGBA{0, 0, 0, 255} - } - display.SetPixel(x, y, c) - display.Display() - - x += deltaX - y += deltaY - - if x == 0 || x == 127 { - deltaX = -deltaX - } - - if y == 0 || y == 63 { - deltaY = -deltaY - } - time.Sleep(1 * time.Millisecond) - } -} diff --git a/examples/ssd1306/spi_thumby/main.go b/examples/ssd1306/spi_thumby/main.go deleted file mode 100644 index b2a41e5..0000000 --- a/examples/ssd1306/spi_thumby/main.go +++ /dev/null @@ -1,50 +0,0 @@ -// This example using the SSD1306 OLED display over SPI on the Thumby board -// A very tiny 72x40 display. -package main - -import ( - "image/color" - "machine" - "time" - - "tinygo.org/x/drivers/ssd1306" -) - -func main() { - machine.SPI0.Configure(machine.SPIConfig{}) - display := ssd1306.NewSPI(machine.SPI0, machine.THUMBY_DC_PIN, machine.THUMBY_RESET_PIN, machine.THUMBY_CS_PIN) - display.Configure(ssd1306.Config{ - Width: 72, - Height: 40, - ResetCol: ssd1306.ResetValue{28, 99}, - ResetPage: ssd1306.ResetValue{0, 5}, - }) - - display.ClearDisplay() - - x := int16(36) - y := int16(20) - deltaX := int16(1) - deltaY := int16(1) - for { - pixel := display.GetPixel(x, y) - c := color.RGBA{255, 255, 255, 255} - if pixel { - c = color.RGBA{0, 0, 0, 255} - } - display.SetPixel(x, y, c) - display.Display() - - x += deltaX - y += deltaY - - if x == 0 || x == 71 { - deltaX = -deltaX - } - - if y == 0 || y == 39 { - deltaY = -deltaY - } - time.Sleep(1 * time.Millisecond) - } -} diff --git a/smoketest.sh b/smoketest.sh index f21e2be..01cc2a9 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -65,8 +65,9 @@ tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/shifter/ tinygo build -size short -o ./build/test.hex -target=microbit ./examples/sht3x/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/sht4x/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/shtc3/main.go -tinygo build -size short -o ./build/test.hex -target=microbit ./examples/ssd1306/i2c_128x32/main.go -tinygo build -size short -o ./build/test.hex -target=microbit ./examples/ssd1306/spi_128x64/main.go +tinygo build -size short -o ./build/test.hex -target=xiao-ble ./examples/ssd1306/ +tinygo build -size short -o ./build/test.hex -target=xiao-rp2040 ./examples/ssd1306/ +tinygo build -size short -o ./build/test.hex -target=thumby ./examples/ssd1306/ tinygo build -size short -o ./build/test.hex -target=microbit ./examples/ssd1331/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/st7735/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/st7789/main.go diff --git a/ssd1306/ssd1306.go b/ssd1306/ssd1306.go index dd8ebeb..2e65a3a 100644 --- a/ssd1306/ssd1306.go +++ b/ssd1306/ssd1306.go @@ -6,11 +6,9 @@ package ssd1306 // import "tinygo.org/x/drivers/ssd1306" import ( "errors" "image/color" - "machine" "time" "tinygo.org/x/drivers" - "tinygo.org/x/drivers/internal/legacy" "tinygo.org/x/drivers/pixel" ) @@ -23,16 +21,15 @@ type ResetValue [2]byte // Device wraps I2C or SPI connection. type Device struct { - bus Buser - buffer []byte - width int16 - height int16 - bufferSize int16 - vccState VccMode - canReset bool - resetCol ResetValue - resetPage ResetValue - rotation drivers.Rotation + bus Buser + buffer []byte + width int16 + height int16 + vccState VccMode + canReset bool + resetCol ResetValue + resetPage ResetValue + rotation drivers.Rotation } // Config is the configuration for the display @@ -51,51 +48,15 @@ type Config struct { Rotation drivers.Rotation } -type I2CBus struct { - wire drivers.I2C - Address uint16 -} - -type SPIBus struct { - wire drivers.SPI - dcPin machine.Pin - resetPin machine.Pin - csPin machine.Pin -} - type Buser interface { - configure() error - tx(data []byte, isCommand bool) error - setAddress(address uint16) error + configure(address uint16, size int16) []byte // configure the bus and return the image buffer to use + command(cmd uint8) error // send a command to the display + flush() error // send the image to the display, faster than "tx()" in i2c case since avoids slice copy + tx(data []byte, isCommand bool) error // generic transmit function } type VccMode uint8 -// NewI2C creates a new SSD1306 connection. The I2C wire must already be configured. -func NewI2C(bus drivers.I2C) Device { - return Device{ - bus: &I2CBus{ - wire: bus, - Address: Address, - }, - } -} - -// NewSPI creates a new SSD1306 connection. The SPI wire must already be configured. -func NewSPI(bus drivers.SPI, dcPin, resetPin, csPin machine.Pin) Device { - dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - return Device{ - bus: &SPIBus{ - wire: bus, - dcPin: dcPin, - resetPin: resetPin, - csPin: csPin, - }, - } -} - // Configure initializes the display with default configuration func (d *Device) Configure(cfg Config) { var zeroReset ResetValue @@ -109,9 +70,6 @@ func (d *Device) Configure(cfg Config) { } else { d.height = 64 } - if cfg.Address != 0 { - d.bus.setAddress(cfg.Address) - } if cfg.VccState != 0 { d.vccState = cfg.VccState } else { @@ -127,11 +85,9 @@ func (d *Device) Configure(cfg Config) { } else { d.resetPage = ResetValue{0, uint8(d.height/8) - 1} } - d.bufferSize = d.width * d.height / 8 - d.buffer = make([]byte, d.bufferSize) d.canReset = cfg.Address != 0 || d.width != 128 || d.height != 64 // I2C or not 128x64 - d.bus.configure() + d.buffer = d.bus.configure(cfg.Address, d.width*d.height/8) time.Sleep(100 * time.Nanosecond) d.Command(DISPLAYOFF) @@ -193,11 +149,22 @@ func (d *Device) Configure(cfg Config) { d.Command(NORMALDISPLAY) d.Command(DEACTIVATE_SCROLL) d.Command(DISPLAYON) + +} + +// Command sends a command to the display +func (d *Device) Command(command uint8) { + d.bus.command(command) +} + +// Tx sends data to the display; if isCommand is false, this also updates the image buffer. +func (d *Device) Tx(data []byte, isCommand bool) error { + return d.bus.tx(data, isCommand) } // ClearBuffer clears the image buffer func (d *Device) ClearBuffer() { - for i := int16(0); i < d.bufferSize; i++ { + for i := 0; i < len(d.buffer); i++ { d.buffer[i] = 0 } } @@ -223,7 +190,7 @@ func (d *Device) Display() error { d.Command(d.resetPage[1]) } - return d.Tx(d.buffer, false) + return d.bus.flush() } // SetPixel enables or disables a pixel in the buffer @@ -252,12 +219,10 @@ func (d *Device) GetPixel(x int16, y int16) bool { // SetBuffer changes the whole buffer at once func (d *Device) SetBuffer(buffer []byte) error { - if int16(len(buffer)) != d.bufferSize { + if len(buffer) != len(d.buffer) { return errBufferSize } - for i := int16(0); i < d.bufferSize; i++ { - d.buffer[i] = buffer[i] - } + copy(d.buffer, buffer) return nil } @@ -266,79 +231,6 @@ func (d *Device) GetBuffer() []byte { return d.buffer } -// Command sends a command to the display -func (d *Device) Command(command uint8) { - d.bus.tx([]byte{command}, true) -} - -// setAddress sets the address to the I2C bus -func (b *I2CBus) setAddress(address uint16) error { - b.Address = address - return nil -} - -// setAddress does nothing, but it's required to avoid reflection -func (b *SPIBus) setAddress(address uint16) error { - // do nothing - println("trying to Configure an address on a SPI device") - return nil -} - -// configure does nothing, but it's required to avoid reflection -func (b *I2CBus) configure() error { return nil } - -// configure configures some pins with the SPI bus -func (b *SPIBus) configure() error { - b.csPin.Low() - b.dcPin.Low() - b.resetPin.Low() - - b.resetPin.High() - time.Sleep(1 * time.Millisecond) - b.resetPin.Low() - time.Sleep(10 * time.Millisecond) - b.resetPin.High() - - return nil -} - -// Tx sends data to the display -func (d *Device) Tx(data []byte, isCommand bool) error { - return d.bus.tx(data, isCommand) -} - -// tx sends data to the display (I2CBus implementation) -func (b *I2CBus) tx(data []byte, isCommand bool) error { - if isCommand { - return legacy.WriteRegister(b.wire, uint8(b.Address), 0x00, data) - } else { - return legacy.WriteRegister(b.wire, uint8(b.Address), 0x40, data) - } -} - -// tx sends data to the display (SPIBus implementation) -func (b *SPIBus) tx(data []byte, isCommand bool) error { - var err error - - if isCommand { - b.csPin.High() - b.dcPin.Low() - b.csPin.Low() - - err = b.wire.Tx(data, nil) - b.csPin.High() - } else { - b.csPin.High() - b.dcPin.High() - b.csPin.Low() - - err = b.wire.Tx(data, nil) - b.csPin.High() - } - - return err -} - // Size returns the current size of the display. func (d *Device) Size() (w, h int16) { return d.width, d.height diff --git a/ssd1306/ssd1306_i2c.go b/ssd1306/ssd1306_i2c.go new file mode 100644 index 0000000..19f3a1c --- /dev/null +++ b/ssd1306/ssd1306_i2c.go @@ -0,0 +1,52 @@ +package ssd1306 + +import ( + "tinygo.org/x/drivers" +) + +type I2CBus struct { + wire drivers.I2C + address uint16 + buffer []byte // buffer to avoid heap allocations +} + +// NewI2C creates a new SSD1306 connection. The I2C wire must already be configured. +func NewI2C(bus drivers.I2C) *Device { + return &Device{ + bus: &I2CBus{ + wire: bus, + address: Address, + }, + } +} + +// configure address for the I2C bus and allocate the buffer +func (b *I2CBus) configure(address uint16, size int16) []byte { + if address != 0 { + b.address = address + } + b.buffer = make([]byte, size+2) // +1 for the mode and +1 for a command + return b.buffer[2:] // return the image buffer +} + +// command sends a command to the display +func (b *I2CBus) command(cmd uint8) error { + b.buffer[0] = 0x00 // Command mode + b.buffer[1] = cmd + return b.wire.Tx(b.address, b.buffer[:2], nil) +} + +// flush sends the image to the display +func (b *I2CBus) flush() error { + b.buffer[1] = 0x40 // Data mode + return b.wire.Tx(b.address, b.buffer[1:], nil) +} + +// tx sends data to the display +func (b *I2CBus) tx(data []byte, isCommand bool) error { + if isCommand { + return b.command(data[0]) + } + copy(b.buffer[2:], data) + return b.flush() +} diff --git a/ssd1306/ssd1306_spi.go b/ssd1306/ssd1306_spi.go new file mode 100644 index 0000000..d96299d --- /dev/null +++ b/ssd1306/ssd1306_spi.go @@ -0,0 +1,68 @@ +package ssd1306 + +import ( + "machine" + "time" + + "tinygo.org/x/drivers" +) + +type SPIBus struct { + wire drivers.SPI + dcPin machine.Pin + resetPin machine.Pin + csPin machine.Pin + buffer []byte // buffer to avoid heap allocations +} + +// NewSPI creates a new SSD1306 connection. The SPI wire must already be configured. +func NewSPI(bus drivers.SPI, dcPin, resetPin, csPin machine.Pin) *Device { + dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + return &Device{ + bus: &SPIBus{ + wire: bus, + dcPin: dcPin, + resetPin: resetPin, + csPin: csPin, + }, + } +} + +// configure pins with the SPI bus and allocate the buffer +func (b *SPIBus) configure(address uint16, size int16) []byte { + b.csPin.Low() + b.dcPin.Low() + b.resetPin.Low() + + b.resetPin.High() + time.Sleep(1 * time.Millisecond) + b.resetPin.Low() + time.Sleep(10 * time.Millisecond) + b.resetPin.High() + + b.buffer = make([]byte, size+1) // +1 for a command + return b.buffer[1:] // return the image buffer +} + +// command sends a command to the display +func (b *SPIBus) command(cmd uint8) error { + b.buffer[0] = cmd + return b.tx(b.buffer[:1], true) +} + +// flush sends the image to the display +func (b *SPIBus) flush() error { + return b.tx(b.buffer[1:], false) +} + +// tx sends data to the display +func (b *SPIBus) tx(data []byte, isCommand bool) error { + b.csPin.High() + b.dcPin.Set(!isCommand) + b.csPin.Low() + err := b.wire.Tx(data, nil) + b.csPin.High() + return err +} From 28d87eb0c57b4cf5aeeea5c09986bb28621bbbfd Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 16 Jun 2025 17:10:53 +0200 Subject: [PATCH 075/137] ws2812: add RP2350 support Adding 150MHz support for the RP2350 --- ws2812/ws2812-asm_cortexm.go | 360 +++++++++++++++++++++++++++++++++++ ws2812/ws2812.go | 2 +- ws2812/ws2812_cortexm.go | 3 + 3 files changed, 364 insertions(+), 1 deletion(-) diff --git a/ws2812/ws2812-asm_cortexm.go b/ws2812/ws2812-asm_cortexm.go index a13e7fa..1f06c33 100644 --- a/ws2812/ws2812-asm_cortexm.go +++ b/ws2812/ws2812-asm_cortexm.go @@ -931,6 +931,356 @@ void ws2812_writeByte125(char c, uint32_t *portSet, uint32_t *portClear, uint32_ [portClear]"m"(*portClear)); } +__attribute__((always_inline)) +void ws2812_writeByte150(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) { + // Timings: + // T0H: 53 - 55 cycles or 353.3ns - 366.7ns + // T1H: 158 - 160 cycles or 1053.3ns - 1066.7ns + // TLD: 173 - cycles or 1153.3ns - + uint32_t value = (uint32_t)c << 24; + char i = 8; + __asm__ __volatile__( + "1: @ send_bit\n" + "\t str %[maskSet], %[portSet] @ [2] T0H and T0L start here\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t lsls %[value], #1 @ [1]\n" + "\t bcs.n 2f @ [1/3] skip_store\n" + "\t str %[maskClear], %[portClear] @ [2] T0H -> T0L transition\n" + "\t2: @ skip_store\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t str %[maskClear], %[portClear] @ [2] T1H -> T1L transition\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t subs %[i], #1 @ [1]\n" + "\t beq.n 3f @ [1/3] end\n" + "\t b 1b @ [1/3] send_bit\n" + "\t3: @ end\n" + : [value]"+r"(value), + [i]"+r"(i) + : [maskSet]"r"(maskSet), + [portSet]"m"(*portSet), + [maskClear]"r"(maskClear), + [portClear]"m"(*portClear)); +} + __attribute__((always_inline)) void ws2812_writeByte168(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) { // Timings: @@ -1832,6 +2182,16 @@ func (d Device) writeByte125(c byte) { interrupt.Restore(mask) } +func (d Device) writeByte150(c byte) { + portSet, maskSet := d.Pin.PortMaskSet() + portClear, maskClear := d.Pin.PortMaskClear() + + mask := interrupt.Disable() + C.ws2812_writeByte150(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear)) + + interrupt.Restore(mask) +} + func (d Device) writeByte168(c byte) { portSet, maskSet := d.Pin.PortMaskSet() portClear, maskClear := d.Pin.PortMaskClear() diff --git a/ws2812/ws2812.go b/ws2812/ws2812.go index d5860f6..dee3cfb 100644 --- a/ws2812/ws2812.go +++ b/ws2812/ws2812.go @@ -1,7 +1,7 @@ // Package ws2812 implements a driver for WS2812 and SK6812 RGB LED strips. package ws2812 // import "tinygo.org/x/drivers/ws2812" -//go:generate go run gen-ws2812.go -arch=cortexm 16 48 64 120 125 168 200 +//go:generate go run gen-ws2812.go -arch=cortexm 16 48 64 120 125 150 168 200 //go:generate go run gen-ws2812.go -arch=tinygoriscv 160 320 import ( diff --git a/ws2812/ws2812_cortexm.go b/ws2812/ws2812_cortexm.go index b818a67..b1f6ed2 100644 --- a/ws2812/ws2812_cortexm.go +++ b/ws2812/ws2812_cortexm.go @@ -31,6 +31,9 @@ func (d Device) WriteByte(c byte) error { case 125_000_000: // 125 MHz e.g. rp2040 originally d.writeByte125(c) return nil + case 150_000_000: // 150MHz, e.g. rp2350 + d.writeByte150(c) + return nil case 168_000_000: // 168MHz, e.g. stm32f405 d.writeByte168(c) return nil From 833990f44dfaa458c229310f4703c76b381cc538 Mon Sep 17 00:00:00 2001 From: Artur Nasyrov Date: Fri, 25 Jul 2025 15:54:43 +0300 Subject: [PATCH 076/137] Add ens160 i2c driver Driver for ENS160 sensor: https://www.sciosense.com/wp-content/uploads/2023/12/ENS160-Datasheet.pdf --- ens160/ens160.go | 225 ++++++++++++++++++++++++++++++++++++++++ ens160/ens160_test.go | 54 ++++++++++ ens160/registers.go | 65 ++++++++++++ examples/ens160/main.go | 56 ++++++++++ smoketest.sh | 1 + 5 files changed, 401 insertions(+) create mode 100644 ens160/ens160.go create mode 100644 ens160/ens160_test.go create mode 100644 ens160/registers.go create mode 100644 examples/ens160/main.go diff --git a/ens160/ens160.go b/ens160/ens160.go new file mode 100644 index 0000000..04f5a33 --- /dev/null +++ b/ens160/ens160.go @@ -0,0 +1,225 @@ +// Package ens160 provides a driver for the ScioSense ENS160 digital gas sensor. +// +// Datasheet: https://www.sciosense.com/wp-content/uploads/2023/12/ENS160-Datasheet.pdf +package ens160 + +import ( + "encoding/binary" + "errors" + "time" + + "tinygo.org/x/drivers" +) + +const ( + defaultTimeout = 30 * time.Millisecond + shortTimeout = 1 * time.Millisecond +) + +// Conversion constants for environment data compensation. +const ( + kelvinOffsetMilli = 273150 // 273.15 K in milli-units + tempRawFactor = 64 // As per datasheet for TEMP_IN + humRawFactor = 512 // As per datasheet for RH_IN + milliFactor = 1000 // For converting from milli-units + roundingTerm = milliFactor / 2 // For rounding before integer division +) + +// validityStrings provides human-readable descriptions for validity flags. +var validityStrings = [...]string{ + ValidityNormalOperation: "normal operation", + ValidityWarmUpPhase: "warm-up phase, wait ~3 minutes for valid data", + ValidityInitialStartUpPhase: "initial start-up phase, wait ~1 hour for valid data", + ValidityInvalidOutput: "invalid output", +} + +// Device wraps an I2C connection to an ENS160 device. +type Device struct { + bus drivers.I2C // I²C implementation + addr uint16 // 7‑bit bus address, promoted to uint16 per drivers.I2C + + // shadow registers / last measurements + lastTvocPPB uint16 + lastEco2PPM uint16 + lastAqiUBA uint8 + lastValidity uint8 // Store the latest validity status + + // pre‑allocated buffers + wbuf [5]byte // longest write: reg + 4 bytes (TEMP+RH) + rbuf [5]byte // longest read: DATA burst (5 bytes) +} + +// New returns a new ENS160 driver. +func New(bus drivers.I2C, addr uint16) *Device { + if addr == 0 { + addr = DefaultAddress + } + return &Device{ + bus: bus, + addr: addr, + lastValidity: ValidityInvalidOutput, + } +} + +// Connected returns whether a ENS160 has been found. +func (d *Device) Connected() bool { + d.wbuf[0] = regPartID + err := d.bus.Tx(d.addr, d.wbuf[:1], d.rbuf[:2]) + return err == nil && d.rbuf[0] == LowPartID && d.rbuf[1] == HighPartID +} + +// Configure sets up the device for reading. +func (d *Device) Configure() error { + // 1. Soft-reset. The device will automatically enter IDLE mode. + if err := d.write1(regOpMode, ModeReset); err != nil { + return err + } + time.Sleep(defaultTimeout) + + // 2. Clear GPR registers, then go to STANDARD mode. + if err := d.write1(regCommand, cmdClrGPR); err != nil { + return err + } + time.Sleep(defaultTimeout) + + if err := d.write1(regOpMode, ModeStandard); err != nil { + return err + } + time.Sleep(defaultTimeout) + + return nil +} + +// calculateTempRaw converts temperature from milli-degrees Celsius to the sensor's raw format. +func calculateTempRaw(tempMilliC int32) uint16 { + // Clip temperature + const ( + minC = -40 * 1000 + maxC = 85 * 1000 + ) + if tempMilliC < minC { + tempMilliC = minC + } else if tempMilliC > maxC { + tempMilliC = maxC + } + + // Integer fixed-point conversion to format required by the sensor. + // Formula from datasheet: T_IN = (T_ambient_C + 273.15) * 64 + return uint16((((tempMilliC + kelvinOffsetMilli) * tempRawFactor) + roundingTerm) / milliFactor) +} + +// calculateHumRaw converts relative humidity from milli-percent to the sensor's raw format. +func calculateHumRaw(rhMilliPct int32) uint16 { + // Clip humidity + if rhMilliPct < 0 { + rhMilliPct = 0 + } else if rhMilliPct > 100*1000 { + rhMilliPct = 100 * 1000 + } + + // Integer fixed-point conversion to format required by the sensor. + // Formula from datasheet: RH_IN = (RH_ambient_% * 512) + return uint16(((rhMilliPct * humRawFactor) + roundingTerm) / milliFactor) +} + +// SetEnvDataMilli sets the ambient temperature and humidity for compensation. +// +// tempMilliC is the temperature in milli-degrees Celsius. +// rhMilliPct is the relative humidity in milli-percent. +func (d *Device) SetEnvDataMilli(tempMilliC, rhMilliPct int32) error { + tempRaw := calculateTempRaw(tempMilliC) + humRaw := calculateHumRaw(rhMilliPct) + + d.wbuf[0] = regTempIn // start address (auto‑increment) + binary.LittleEndian.PutUint16(d.wbuf[1:3], tempRaw) + binary.LittleEndian.PutUint16(d.wbuf[3:5], humRaw) + + return d.bus.Tx(d.addr, d.wbuf[:5], nil) +} + +// Update refreshes the concentration measurements. +func (d *Device) Update(which drivers.Measurement) error { + if which&drivers.Concentration == 0 { + return nil // nothing requested + } + + const maxTries = 1000 + var ( + status uint8 + validity uint8 + ) + var gotData bool + + // Poll DEVICE_STATUS until NEWDAT or timeout + for range maxTries { + var err error + status, err = d.read1(regStatus) + if err != nil { + return err + } + if status&statusSTATER != 0 { + return errors.New("ENS160: error (STATER set)") + } + validity = (status & statusValidityMask) >> statusValidityShift + + if status&statusNEWDAT != 0 { + gotData = true + break // Always break when data available + } + time.Sleep(shortTimeout) + } + if !gotData { + return errors.New("ENS160: timeout waiting for NEWDAT") + } + + // Burst-read data regardless of validity state + d.wbuf[0] = regAQI + if err := d.bus.Tx(d.addr, d.wbuf[:1], d.rbuf[:5]); err != nil { + return errors.New("ENS160: burst read failed") + } + + d.lastAqiUBA = d.rbuf[0] + d.lastTvocPPB = binary.LittleEndian.Uint16(d.rbuf[1:3]) + d.lastEco2PPM = binary.LittleEndian.Uint16(d.rbuf[3:5]) + d.lastValidity = validity // Store the validity status + + return nil +} + +// TVOC returns the last total‑VOC concentration in parts‑per‑billion. +func (d *Device) TVOC() uint16 { return d.lastTvocPPB } + +// ECO2 returns the last equivalent CO₂ concentration in parts‑per‑million. +func (d *Device) ECO2() uint16 { return d.lastEco2PPM } + +// AQI returns the last Air‑Quality Index according to UBA (1–5). +func (d *Device) AQI() uint8 { return d.lastAqiUBA } + +// Validity returns the current operating state of the sensor. +func (d *Device) Validity() uint8 { + return d.lastValidity +} + +// ValidityString returns a human-readable string describing the current validity status. +func (d *Device) ValidityString() string { + if int(d.lastValidity) < len(validityStrings) { + return validityStrings[d.lastValidity] + } + return "unknown" +} + +// write1 writes a single byte to a register. +func (d *Device) write1(reg, val uint8) error { + d.wbuf[0] = reg + d.wbuf[1] = val + return d.bus.Tx(d.addr, d.wbuf[:2], nil) +} + +// read1 reads a single byte from a register. +func (d *Device) read1(reg uint8) (uint8, error) { + d.wbuf[0] = reg + if err := d.bus.Tx(d.addr, d.wbuf[:1], d.rbuf[:1]); err != nil { + return 0, err + } + return d.rbuf[0], nil +} diff --git a/ens160/ens160_test.go b/ens160/ens160_test.go new file mode 100644 index 0000000..022de16 --- /dev/null +++ b/ens160/ens160_test.go @@ -0,0 +1,54 @@ +package ens160 + +import ( + "testing" +) + +func TestCalculateTempRaw(t *testing.T) { + testCases := []struct { + name string + tempMilliC int32 + expectedRaw uint16 + }{ + {"25°C", 25000, 19082}, + {"-10.5°C", -10500, 16810}, + {"Min temp", -40000, 14922}, + {"Below min", -50000, 14922}, + {"Max temp", 85000, 22922}, + {"Above max", 90000, 22922}, + {"Zero", 0, 17482}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + raw := calculateTempRaw(tc.tempMilliC) + if raw != tc.expectedRaw { + t.Errorf("expected %d, got %d", tc.expectedRaw, raw) + } + }) + } +} + +func TestCalculateHumRaw(t *testing.T) { + testCases := []struct { + name string + rhMilliPct int32 + expectedRaw uint16 + }{ + {"50%", 50000, 25600}, + {"0%", 0, 0}, + {"100%", 100000, 51200}, + {"Below 0%", -10000, 0}, + {"Above 100%", 110000, 51200}, + {"33.3%", 33300, 17050}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + raw := calculateHumRaw(tc.rhMilliPct) + if raw != tc.expectedRaw { + t.Errorf("expected %d, got %d", tc.expectedRaw, raw) + } + }) + } +} diff --git a/ens160/registers.go b/ens160/registers.go new file mode 100644 index 0000000..0650c91 --- /dev/null +++ b/ens160/registers.go @@ -0,0 +1,65 @@ +package ens160 + +// DefaultAddress is the default I2C address for the ENS160 when the ADDR pin is +// connected to high (3.3V). When connected to low (GND), the address is 0x52. +const DefaultAddress = 0x53 + +// Registers +const ( + regPartID = 0x00 + regOpMode = 0x10 + regConfig = 0x11 + regCommand = 0x12 + regTempIn = 0x13 + regRhIn = 0x15 + regStatus = 0x20 + regAQI = 0x21 + regTVOC = 0x22 + regECO2 = 0x24 + regDataT = 0x30 + regDataRH = 0x32 + regMISR = 0x38 + regGPRWrite = 0x40 + regGPRRead = 0x48 +) + +// Operating modes +const ( + ModeDeepSleep = 0x00 + ModeIdle = 0x01 + ModeStandard = 0x02 + ModeReset = 0xF0 +) + +// Status register bits +const ( + statusSTATAS = 1 << 7 + statusSTATER = 1 << 6 + + statusValidityMask = 0x0C + statusValidityShift = 2 + + statusNEWDAT = 1 << 1 + statusNEWGPR = 1 << 0 +) + +// Validity flags +const ( + ValidityNormalOperation = 0x00 + ValidityWarmUpPhase = 0x01 // need ~3 minutes until valid data + ValidityInitialStartUpPhase = 0x02 // need ~1 hour until valid data + ValidityInvalidOutput = 0x03 +) + +// Commands +const ( + cmdNOP = 0x00 + cmdGetAppVer = 0x0E + cmdClrGPR = 0xCC +) + +// Part IDs +const ( + LowPartID = 0x60 + HighPartID = 0x01 +) diff --git a/examples/ens160/main.go b/examples/ens160/main.go new file mode 100644 index 0000000..d357160 --- /dev/null +++ b/examples/ens160/main.go @@ -0,0 +1,56 @@ +// This example demonstrates ENS160 usage. +// +// Wiring: +// - VCC to 3.3V, GND to ground +// - SDA to board SDA, SCL to board SCL + +package main + +import ( + "time" + + "machine" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/ens160" +) + +func main() { + err := machine.I2C0.Configure(machine.I2CConfig{ + Frequency: 400 * machine.KHz, + }) + if err != nil { + println("Failed to configure I2C:", err) + } + + dev := ens160.New(machine.I2C0, ens160.DefaultAddress) + + connected := dev.Connected() + if !connected { + println("ENS160 not detected") + return + } + println("ENS160 detected") + + if err := dev.Configure(); err != nil { + println("Failed to configure ENS160:", err) + } + + for { + err := dev.Update(drivers.Concentration) + if err != nil { + println("Error reading ENS160: %v\n", err) + time.Sleep(5 * time.Second) + continue + } + + println( + "AQI:", dev.AQI(), + "TVOC:", dev.TVOC(), + "eCO2:", dev.ECO2(), + "Validity:", dev.ValidityString(), + ) + + time.Sleep(2 * time.Second) + } +} diff --git a/smoketest.sh b/smoketest.sh index 01cc2a9..fef87ce 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -141,6 +141,7 @@ tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/tmc2209/mai tinygo build -size short -o ./build/test.hex -target=pico ./examples/tmc5160/main.go 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 # network examples (espat) tinygo build -size short -o ./build/test.hex -target=challenger-rp2040 ./examples/net/ntpclient/ # network examples (wifinina) From 303ec945295801ddad286259d55aec12122f4ed3 Mon Sep 17 00:00:00 2001 From: Bryan Souza <40864284+BryanSouza91@users.noreply.github.com> Date: Sun, 10 Aug 2025 23:47:22 -0700 Subject: [PATCH 077/137] added support for LSM303DLHC e-Compass; (#783) fixed the spelling in the Connection error message; Initial support for LSM303DLHC added; Added LSM303DLHC to smoketest and added an example; Removed unnecessary comments; fixed format error; squashed and ready for merge; --- examples/lsm303dlhc/main.go | 58 ++++++++++ lsm303agr/lsm303agr.go | 2 +- lsm303dlhc/lsm303dlhc.go | 214 ++++++++++++++++++++++++++++++++++++ lsm303dlhc/registers.go | 75 +++++++++++++ smoketest.sh | 1 + 5 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 examples/lsm303dlhc/main.go create mode 100644 lsm303dlhc/lsm303dlhc.go create mode 100644 lsm303dlhc/registers.go diff --git a/examples/lsm303dlhc/main.go b/examples/lsm303dlhc/main.go new file mode 100644 index 0000000..9e27eaf --- /dev/null +++ b/examples/lsm303dlhc/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/lsm303dlhc" +) + +func main() { + + // LSM303DLHC is connected to the I2C0 bus on Adafruit Feather M4 via pins: 20(SDA) and 21(SCL). + machine.I2C0.Configure(machine.I2CConfig{}) + + sensor := lsm303dlhc.New(machine.I2C0) + //default settings + err := sensor.Configure(lsm303dlhc.Configuration{ + AccelPowerMode: lsm303dlhc.ACCEL_POWER_NORMAL, + AccelRange: lsm303dlhc.ACCEL_RANGE_2G, + AccelDataRate: lsm303dlhc.ACCEL_DATARATE_100HZ, + MagPowerMode: lsm303dlhc.MAG_POWER_NORMAL, + MagSystemMode: lsm303dlhc.MAG_SYSTEM_CONTINUOUS, + MagDataRate: lsm303dlhc.MAG_DATARATE_10HZ, + }) + if err != nil { + for { + println("Failed to configure", err.Error()) + time.Sleep(time.Second) + } + } + + for { + accel_x, accel_y, accel_z, err := sensor.ReadAcceleration() + if err != nil { + println("Failed to read accel", err.Error()) + } + println("ACCEL_X:", accel_x, " ACCEL_Y:", accel_y, " ACCEL_Z:", accel_z) + + mag_x, mag_y, mag_z, err := sensor.ReadMagneticField() + if err != nil { + println("Failed to read mag", err.Error()) + } + println("MAG_X:", mag_x, " MAG_Y:", mag_y, " MAG_Z:", mag_z) + + pitch, roll, _ := sensor.ReadPitchRoll() + println("Pitch:", float32(pitch), " Roll:", float32(roll)) + + heading, _ := sensor.ReadCompass() + println("Heading:", float32(heading), "degrees") + + temp, _ := sensor.ReadTemperature() + println("Temperature:", float32(temp)/1000, "*C") + + println("\n") + time.Sleep(time.Millisecond * 250) + } + +} diff --git a/lsm303agr/lsm303agr.go b/lsm303agr/lsm303agr.go index 35a4c92..f6848eb 100644 --- a/lsm303agr/lsm303agr.go +++ b/lsm303agr/lsm303agr.go @@ -36,7 +36,7 @@ type Configuration struct { MagDataRate uint8 } -var errNotConnected = errors.New("lsm303agr: failed to communicate with either acel or magnet sensor") +var errNotConnected = errors.New("lsm303agr: failed to communicate with either accel or magnet sensor") // New creates a new LSM303AGR connection. The I2C bus must already be configured. // diff --git a/lsm303dlhc/lsm303dlhc.go b/lsm303dlhc/lsm303dlhc.go new file mode 100644 index 0000000..6e0c2fa --- /dev/null +++ b/lsm303dlhc/lsm303dlhc.go @@ -0,0 +1,214 @@ +// Package lsm303dlhc implements a driver for the LSM303dlhc, +// a 3 axis accelerometer/magnetic sensor typically available on breakout boards. +// +// Datasheet: https://www.st.com/resource/en/datasheet/lsm303dlhc.pdf + +package lsm303dlhc // import "tinygo.org/x/drivers/lsm303dlhc" + +import ( + "math" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" +) + +// Device wraps an I2C connection to a LSM303dlhc device. +type Device struct { + bus drivers.I2C + AccelAddress uint8 + MagAddress uint8 + AccelPowerMode uint8 + AccelRange uint8 + AccelDataRate uint8 + MagPowerMode uint8 + MagSystemMode uint8 + MagDataRate uint8 + buf [6]uint8 +} + +// Configuration for LSM303dlhc device. +type Configuration struct { + AccelPowerMode uint8 + AccelRange uint8 + AccelDataRate uint8 + MagPowerMode uint8 + MagSystemMode uint8 + MagDataRate uint8 +} + +// New creates a new LSM303DLHC connection. The I2C bus must already be configured. +// This function only creates the Device object, it does not touch the device. +func New(bus drivers.I2C) *Device { + return &Device{ + bus: bus, + AccelAddress: ACCEL_ADDRESS, + MagAddress: MAG_ADDRESS, + } +} + +// Configure sets up the LSM303dlhc device for communication. +func (d *Device) Configure(cfg Configuration) (err error) { + + if cfg.AccelDataRate != 0 { + d.AccelDataRate = cfg.AccelDataRate + } else { + d.AccelDataRate = ACCEL_DATARATE_100HZ + } + + if cfg.AccelPowerMode != 0 { + d.AccelPowerMode = cfg.AccelPowerMode + } else { + d.AccelPowerMode = ACCEL_POWER_NORMAL + } + + if cfg.AccelRange != 0 { + d.AccelRange = cfg.AccelRange + } else { + d.AccelRange = ACCEL_RANGE_2G + } + + if cfg.MagPowerMode != 0 { + d.MagPowerMode = cfg.MagPowerMode + } else { + d.MagPowerMode = MAG_POWER_NORMAL + } + + if cfg.MagDataRate != 0 { + d.MagDataRate = cfg.MagDataRate + } else { + d.MagDataRate = MAG_DATARATE_10HZ + } + + if cfg.MagSystemMode != 0 { + d.MagSystemMode = cfg.MagSystemMode + } else { + d.MagSystemMode = MAG_SYSTEM_CONTINUOUS + } + + data := d.buf[:1] + + data[0] = byte(d.AccelDataRate<<4 | d.AccelPowerMode | 0x07) + err = legacy.WriteRegister(d.bus, uint8(d.AccelAddress), ACCEL_CTRL_REG1_A, data) + if err != nil { + return + } + + data[0] = byte(0x80 | d.AccelRange<<4) + err = legacy.WriteRegister(d.bus, uint8(d.AccelAddress), ACCEL_CTRL_REG4_A, data) + if err != nil { + return + } + + data[0] = byte(0xC0) + err = legacy.WriteRegister(d.bus, uint8(d.AccelAddress), CRA_REG_M, data) + if err != nil { + return + } + + // Temperature compensation is on for magnetic sensor + data[0] = byte(0x80 | d.MagPowerMode<<4 | d.MagDataRate<<2 | d.MagSystemMode) + err = legacy.WriteRegister(d.bus, uint8(d.MagAddress), MAG_MR_REG_M, data) + if err != nil { + return + } + + return nil +} + +// ReadAcceleration reads the current acceleration from the device and returns +// it in µg (micro-gravity). When one of the axes is pointing straight to Earth +// and the sensor is not moving the returned value will be around 1000000 or +// -1000000. +func (d *Device) ReadAcceleration() (x, y, z int32, err error) { + data := d.buf[:6] + err = legacy.ReadRegister(d.bus, uint8(d.AccelAddress), ACCEL_OUT_AUTO_INC, data) + if err != nil { + return + } + + rangeFactor := int16(0) + switch d.AccelRange { + case ACCEL_RANGE_2G: + rangeFactor = 1 + case ACCEL_RANGE_4G: + rangeFactor = 2 + case ACCEL_RANGE_8G: + rangeFactor = 4 + case ACCEL_RANGE_16G: + rangeFactor = 12 // the readings in 16G are a bit lower + } + + x = int32(int32(int16((uint16(data[1])<<8|uint16(data[0])))>>4*rangeFactor) * 1000000 / 1024) + y = int32(int32(int16((uint16(data[3])<<8|uint16(data[2])))>>4*rangeFactor) * 1000000 / 1024) + z = int32(int32(int16((uint16(data[5])<<8|uint16(data[4])))>>4*rangeFactor) * 1000000 / 1024) + return +} + +// ReadPitchRoll reads the current pitch and roll angles from the device and +// returns it in micro-degrees. When the z axis is pointing straight to Earth +// the returned values of pitch and roll would be zero. +func (d *Device) ReadPitchRoll() (pitch, roll int32, err error) { + + x, y, z, err := d.ReadAcceleration() + if err != nil { + return + } + xf, yf, zf := float64(x), float64(y), float64(z) + pitch = int32((math.Round(math.Atan2(yf, math.Sqrt(math.Pow(xf, 2)+math.Pow(zf, 2)))*(180/math.Pi)*100) / 100) * 1000000) + roll = int32((math.Round(math.Atan2(xf, math.Sqrt(math.Pow(yf, 2)+math.Pow(zf, 2)))*(180/math.Pi)*100) / 100) * 1000000) + return + +} + +// ReadMagneticField reads the current magnetic field from the device and returns +// it in mG (milligauss). 1 mG = 0.1 µT (microtesla). +func (d *Device) ReadMagneticField() (x, y, z int32, err error) { + + if d.MagSystemMode == MAG_SYSTEM_SINGLE { + cmd := d.buf[:1] + cmd[0] = byte(0x80 | d.MagPowerMode<<4 | d.MagDataRate<<2 | d.MagSystemMode) + err = legacy.WriteRegister(d.bus, uint8(d.MagAddress), MAG_MR_REG_M, cmd) + if err != nil { + return + } + } + + data := d.buf[0:6] + legacy.ReadRegister(d.bus, uint8(d.MagAddress), MAG_OUT_AUTO_INC, data) + + x = int32(int16((uint16(data[1])<<8 | uint16(data[0])))) + y = int32(int16((uint16(data[3])<<8 | uint16(data[2])))) + z = int32(int16((uint16(data[5])<<8 | uint16(data[4])))) + return +} + +// ReadCompass reads the current compass heading from the device and returns +// it in micro-degrees. When the z axis is pointing straight to Earth and +// the y axis is pointing to North, the heading would be zero. +// +// However, the heading may be off due to electronic compasses would be effected +// by strong magnetic fields and require constant calibration. +func (d *Device) ReadCompass() (h int32, err error) { + + x, y, _, err := d.ReadMagneticField() + if err != nil { + return + } + xf, yf := float64(x), float64(y) + h = int32(float32((180/math.Pi)*math.Atan2(yf, xf)) * 1000000) + return +} + +// ReadTemperature returns the temperature in Celsius milli degrees (°C/1000) +func (d *Device) ReadTemperature() (t int32, err error) { + + data := d.buf[:2] + err = legacy.ReadRegister(d.bus, uint8(d.MagAddress), TEMP_OUT_AUTO_INC, data) + if err != nil { + return + } + + r := int16((uint16(data[1])<<8 | uint16(data[0]))) >> 4 // temperature offset from 25 °C + t = 25000 + int32((float32(r)/8)*1000) + return +} diff --git a/lsm303dlhc/registers.go b/lsm303dlhc/registers.go new file mode 100644 index 0000000..09206f3 --- /dev/null +++ b/lsm303dlhc/registers.go @@ -0,0 +1,75 @@ +package lsm303dlhc + +const ( + + // Constants/addresses used for I2C. + ACCEL_ADDRESS = 0x19 + MAG_ADDRESS = 0x1E + + // i2C 8-bit subaddress (SUB): the 7 LSb represent the actual register address + // while the MSB enables address auto increment. + // If the MSb of the SUB field is 1, the SUB (register address) is + // automatically increased to allow multiple data read/writes. + ADDR_AUTO_INC_MASK = 0x80 + + // accelerometer registers. + ACCEL_CTRL_REG1_A = 0x20 + ACCEL_CTRL_REG4_A = 0x23 + ACCEL_OUT_X_L_A = 0x28 + ACCEL_OUT_X_H_A = 0x29 + ACCEL_OUT_Y_L_A = 0x2A + ACCEL_OUT_Y_H_A = 0x2B + ACCEL_OUT_Z_L_A = 0x2C + ACCEL_OUT_Z_H_A = 0x2D + ACCEL_OUT_AUTO_INC = ACCEL_OUT_X_L_A | ADDR_AUTO_INC_MASK + + // magnetic sensor registers. + MAG_MR_REG_M = 0x02 + MAG_OUT_X_L_M = 0x68 + MAG_OUT_X_H_M = 0x69 + MAG_OUT_Y_L_M = 0x6A + MAG_OUT_Y_H_M = 0x6B + MAG_OUT_Z_L_M = 0x6C + MAG_OUT_Z_H_M = 0x6D + MAG_OUT_AUTO_INC = MAG_OUT_X_L_M | ADDR_AUTO_INC_MASK + + // temperature sensor registers. + CRA_REG_M = 0x80 + TEMP_OUT_L_M = 0x32 + TEMP_OUT_H_M = 0x31 + TEMP_OUT_AUTO_INC = TEMP_OUT_L_M | ADDR_AUTO_INC_MASK + + // accelerometer power mode. + ACCEL_POWER_NORMAL = 0x00 // default + ACCEL_POWER_LOW = 0x08 + + // accelerometer range. + ACCEL_RANGE_2G = 0x00 // default + ACCEL_RANGE_4G = 0x01 + ACCEL_RANGE_8G = 0x02 + ACCEL_RANGE_16G = 0x03 + + // accelerometer data rate. + ACCEL_DATARATE_1HZ = 0x01 + ACCEL_DATARATE_10HZ = 0x02 + ACCEL_DATARATE_25HZ = 0x03 + ACCEL_DATARATE_50HZ = 0x04 + ACCEL_DATARATE_100HZ = 0x05 // default + ACCEL_DATARATE_200HZ = 0x06 + ACCEL_DATARATE_400HZ = 0x07 + ACCEL_DATARATE_1344HZ = 0x09 // 5376Hz in low-power mode + + // magnetic sensor power mode. + MAG_POWER_NORMAL = 0x00 // default + MAG_POWER_LOW = 0x01 + + // magnetic sensor operate mode. + MAG_SYSTEM_CONTINUOUS = 0x00 // default + MAG_SYSTEM_SINGLE = 0x01 + + // magnetic sensor data rate + MAG_DATARATE_10HZ = 0x00 // default + MAG_DATARATE_20HZ = 0x01 + MAG_DATARATE_50HZ = 0x02 + MAG_DATARATE_100HZ = 0x03 +) diff --git a/smoketest.sh b/smoketest.sh index fef87ce..8511c27 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -44,6 +44,7 @@ tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/ili9341 tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/lis3dh/main.go tinygo build -size short -o ./build/test.hex -target=nano-33-ble ./examples/lps22hb/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/lsm303agr/main.go +tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/lsm303dlhc/main.go tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/lsm6ds3/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/mag3110/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/mcp23017/main.go From a31ba26a6c34bfc61011e6acdea7c5a4c439d2cf Mon Sep 17 00:00:00 2001 From: Russel Hunter Yukawa Date: Wed, 13 Aug 2025 16:39:18 +0900 Subject: [PATCH 078/137] Fix gps time calculation (#785) * Change test case to match the date patterns where the bug reproduces * Fix RMC date and time calculation --- gps/gpsparser.go | 5 ++++- gps/gpsparser_test.go | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/gps/gpsparser.go b/gps/gpsparser.go index 0cae7d2..6522a5f 100644 --- a/gps/gpsparser.go +++ b/gps/gpsparser.go @@ -96,7 +96,10 @@ func (parser *Parser) Parse(sentence string) (Fix, error) { fix.Speed = findSpeed(fields[7]) fix.Heading = findHeading(fields[8]) date := findDate(fields[9]) - fix.Time = fix.Time.AddDate(date.Year(), int(date.Month()), date.Day()) + fix.Time = date.Add(time.Duration(fix.Time.Hour())*time.Hour + + time.Duration(fix.Time.Minute())*time.Minute + + time.Duration(fix.Time.Second())*time.Second + + time.Duration(fix.Time.Nanosecond())*time.Nanosecond) return fix, nil } diff --git a/gps/gpsparser_test.go b/gps/gpsparser_test.go index 17c5dd9..9544312 100644 --- a/gps/gpsparser_test.go +++ b/gps/gpsparser_test.go @@ -70,15 +70,15 @@ func TestParseRMC(t *testing.T) { t.Error("should have errInvalidRMCSentence error") } - val = "$GPRMC,203522.00,A,5109.0262308,N,11401.8407342,W,0.004,133.4,130522,0.0,E,D*2B" + val = "$GPRMC,203522.00,A,5109.0262308,N,11401.8407342,W,0.004,133.4,010622,0.0,E,D*2B" fix, err := p.Parse(val) if err != nil { t.Error("should have parsed") } c.Assert(fix.Time.Year(), qt.Equals, 2022) - c.Assert(fix.Time.Month(), qt.Equals, time.May) - c.Assert(fix.Time.Day(), qt.Equals, 13) + c.Assert(fix.Time.Month(), qt.Equals, time.June) + c.Assert(fix.Time.Day(), qt.Equals, 1) c.Assert(fix.Time.Hour(), qt.Equals, 20) c.Assert(fix.Time.Minute(), qt.Equals, 35) c.Assert(fix.Time.Second(), qt.Equals, 22) From 857ab80ae633878d86d7b8901fe871ee0f3dba5a Mon Sep 17 00:00:00 2001 From: JP Hastings-Spital Date: Mon, 30 Jun 2025 15:30:11 +0100 Subject: [PATCH 079/137] feat: add support for seesaw encoders Adds the necessary function addresses for reading and writing encoders on a seesaw. Also provides two helper functions to make this easier. --- examples/seesaw/rotary-encoder/main.go | 35 ++++++++++++++++ examples/seesaw/{ => soil-sensor}/main.go | 0 seesaw/encoder.go | 49 +++++++++++++++++++++++ seesaw/registers.go | 10 +++++ 4 files changed, 94 insertions(+) create mode 100644 examples/seesaw/rotary-encoder/main.go rename examples/seesaw/{ => soil-sensor}/main.go (100%) create mode 100644 seesaw/encoder.go diff --git a/examples/seesaw/rotary-encoder/main.go b/examples/seesaw/rotary-encoder/main.go new file mode 100644 index 0000000..fcd4571 --- /dev/null +++ b/examples/seesaw/rotary-encoder/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/seesaw" +) + +// example reading the position of a rotary encoder (4991) powered by a seesaw +// https://learn.adafruit.com/adafruit-i2c-qt-rotary-encoder/arduino +func main() { + // This assumes you are using an Adafruit QT Py RP2040 for its Stemma QT connector + // https://www.adafruit.com/product/4900 + i2c := machine.I2C1 + i2c.Configure(machine.I2CConfig{ + SCL: machine.I2C1_QT_SCL_PIN, + SDA: machine.I2C1_QT_SDA_PIN, + }) + + dev := seesaw.New(i2c) + dev.Address = 0x36 + + for { + time.Sleep(time.Second) + + pos, err := dev.GetEncoderPosition(0, false) + if err != nil { + println(err) + continue + } + + println(pos) + } +} diff --git a/examples/seesaw/main.go b/examples/seesaw/soil-sensor/main.go similarity index 100% rename from examples/seesaw/main.go rename to examples/seesaw/soil-sensor/main.go diff --git a/seesaw/encoder.go b/seesaw/encoder.go new file mode 100644 index 0000000..dbb7222 --- /dev/null +++ b/seesaw/encoder.go @@ -0,0 +1,49 @@ +package seesaw + +import ( + "errors" +) + +var errInvalidEncoderNumber = errors.New("invalid encoder choice, 0-15 are supported") + +// GetEncoderPosition returns the absolute position (or delta since the previous call) of the specified rotary encoder. +func (d *Device) GetEncoderPosition(encoder uint, asDelta bool) (int32, error) { + if encoder >= 16 { + return 0, errInvalidEncoderNumber + } + + // The function address' upper nibble is the function, the lower nibble selects which encoder to communicate with + fnAddr := FunctionAddress(encoder) + if asDelta { + fnAddr |= FunctionEncoderDelta + } else { + fnAddr |= FunctionEncoderPosition + } + + var buf [4]byte + err := d.Read(ModuleEncoderBase, fnAddr, buf[:]) + if err != nil { + return 0, err + } + + return int32(buf[0])<<24 | int32(buf[1])<<16 | int32(buf[2])<<8 | int32(buf[3]), nil +} + +// SetEncoderPosition calibrate's the encoder's current absolute position to be whatever the provided position is. +func (d *Device) SetEncoderPosition(encoder uint, position int32) error { + if encoder >= 16 { + return errInvalidEncoderNumber + } + + // The function address' upper nibble is the function, the lower nibble selects which encoder to communicate with + fnAddr := FunctionEncoderPosition | FunctionAddress(encoder) + + buf := [4]byte{ + byte(position >> 24), + byte(position >> 16), + byte(position >> 8), + byte(position), + } + + return d.Write(ModuleEncoderBase, fnAddr, buf[:]) +} diff --git a/seesaw/registers.go b/seesaw/registers.go index 3d1ebe7..abebdf1 100644 --- a/seesaw/registers.go +++ b/seesaw/registers.go @@ -98,3 +98,13 @@ const ( FunctionKeypadCount FunctionAddress = 0x04 FunctionKeypadFifo FunctionAddress = 0x10 ) + +// encoder module function address registers +// these are the defaults for encoder 0, change the lower nibble to address other encoders +// see the Device.GetEncoderPosition and SetEncoderPosition methods for examples. +const ( + FunctionEncoderIntenset FunctionAddress = 0x10 + FunctionEncoderIntenclr FunctionAddress = 0x20 + FunctionEncoderPosition FunctionAddress = 0x30 + FunctionEncoderDelta FunctionAddress = 0x40 +) From 6cf1eb86e504ccbf2cede5f1364227b20b4228fb Mon Sep 17 00:00:00 2001 From: deadprogram Date: Mon, 18 Aug 2025 14:40:34 +0200 Subject: [PATCH 080/137] fix: correct smoke tests for Adafruit Seesaw Signed-off-by: deadprogram --- smoketest.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/smoketest.sh b/smoketest.sh index 8511c27..b003598 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -59,7 +59,8 @@ tinygo build -size short -o ./build/test.hex -target=p1am-100 ./examples/p1am/ma tinygo build -size short -o ./build/test.hex -target=pico ./examples/pca9685/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/pcd8544/setbuffer/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/pcd8544/setpixel/main.go -tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/seesaw +tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/seesaw/soil-sensor +tinygo build -size short -o ./build/test.hex -target=qtpy-rp2040 ./examples/seesaw/rotary-encoder tinygo build -size short -o ./build/test.hex -target=arduino ./examples/servo tinygo build -size short -o ./build/test.hex -target=pico ./examples/sgp30 tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/shifter/main.go From 228e57cf9861a763e79c0f9be44ff23365d5cc95 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 19 Aug 2025 18:03:37 +0200 Subject: [PATCH 081/137] release: prepare for 0.33.0 drivers release Signed-off-by: deadprogram --- CHANGELOG.md | 25 +++++++++++++++++++++++++ version.go | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e0229f..f60ded4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +0.33.0 +--- +- **new devices** + - **ens160** + - Add ens160 i2c driver + - **lsm303dlhc** + - added support for LSM303DLHC e-Compass; (#783) + - **seesaw** + - add support for Adafruit Seesaw encoders + +- **enhancements** + - **ws2812** + - add RP2350 support + - **ssd1306** + - avoid unnecessary heap allocations (#767) + - **gps** + - allow gps init with address + - **lsm6ds3tr** + - avoid unnecessary heap allocations (#766) + +- **bugfixes** + - **gps** + - Fix gps time calculation (#785) + + 0.32.0 --- - **enhancements** diff --git a/version.go b/version.go index 8468fe8..6a85412 100644 --- a/version.go +++ b/version.go @@ -2,4 +2,4 @@ package drivers // Version returns a user-readable string showing the version of the drivers package for support purposes. // Update this value before release of new version of software. -const Version = "0.32.0" +const Version = "0.33.0" From 28d625abfd3334a3c4bcff3a9e38dd43b10b96cf Mon Sep 17 00:00:00 2001 From: Bryan Souza Date: Fri, 12 Sep 2025 09:14:37 -0700 Subject: [PATCH 082/137] added support for W25Q80DV flash chip for xiao-ble target; --- flash/devices.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/flash/devices.go b/flash/devices.go index 69239a6..b6ca601 100644 --- a/flash/devices.go +++ b/flash/devices.go @@ -46,6 +46,8 @@ var DefaultDeviceIdentifier = DeviceIdentifierFunc(func(id JedecID) Attrs { return GD25Q16C() case 0xC84017: return GD25Q64C() + case 0xEF4014: + return W25Q80DV() case 0xEF4015: return W25Q16JVIQ() case 0xEF4016: @@ -380,6 +382,25 @@ func W25Q80DL() Attrs { TotalSize: 1 << 20, // 1 MiB StartUp: 5000 * time.Microsecond, JedecID: JedecID{0xEF, 0x60, 0x14}, + MaxClockSpeedMHz: 80, + QuadEnableBitMask: 0x02, + HasSectorProtection: false, + SupportsFastRead: true, + SupportsQSPI: true, + SupportsQSPIWrites: false, + WriteStatusSplit: false, + SingleStatusByte: false, + } +} + +// Settings for the Winbond W25Q80DV 2MiB SPI flash. +// Datasheet: +// https://www.winbond.com/resource-files/w25q80dv%20dl_revh_10022015.pdf +func W25Q80DV() Attrs { + return Attrs{ + TotalSize: 1 << 21, // 2 MiB + StartUp: 5000 * time.Microsecond, + JedecID: JedecID{0xEF, 0x40, 0x14}, MaxClockSpeedMHz: 104, QuadEnableBitMask: 0x02, HasSectorProtection: false, From b639f7b12e0c0ea276c4170c2bb0b74bbb265aba Mon Sep 17 00:00:00 2001 From: Bryan Souza Date: Fri, 12 Sep 2025 10:13:51 -0700 Subject: [PATCH 083/137] added support for P25Q16H flash chip for xiao-ble target; --- flash/devices.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/flash/devices.go b/flash/devices.go index b6ca601..7140e92 100644 --- a/flash/devices.go +++ b/flash/devices.go @@ -46,6 +46,8 @@ var DefaultDeviceIdentifier = DeviceIdentifierFunc(func(id JedecID) Attrs { return GD25Q16C() case 0xC84017: return GD25Q64C() + case 0x856015: + return P25Q16H() case 0xEF4014: return W25Q80DV() case 0xEF4015: @@ -241,6 +243,24 @@ func GD25Q64C() Attrs { } } +// Settings for the Puya P25Q16H 2MiB SPI flash. +// Datasheet: https://files.seeedstudio.com/wiki/github_weiruanexample/Flash_P25Q16H-UXH-IR_Datasheet.pdf +func P25Q16H() Attrs { + return Attrs{ + TotalSize: 1 << 21, // 2 MiB + StartUp: 5000 * time.Microsecond, + JedecID: JedecID{0x85, 0x60, 0x15}, + MaxClockSpeedMHz: 55, + QuadEnableBitMask: 0x02, + HasSectorProtection: true, + SupportsFastRead: true, + SupportsQSPI: true, + SupportsQSPIWrites: true, + WriteStatusSplit: true, + SingleStatusByte: false, + } +} + // Settings for the Winbond W25Q16JV-IQ 2MiB SPI flash. Note that JV-IM has a // different .memory_type (0x70) Datasheet: // https://www.winbond.com/resource-files/w25q16jv%20spi%20revf%2005092017.pdf From 3fa08112db9447eec18b6c183462b1c2dcfb10c0 Mon Sep 17 00:00:00 2001 From: soypat Date: Sat, 21 Jun 2025 10:32:47 -0300 Subject: [PATCH 084/137] add regmap package to facilitate heapless driver development --- internal/regmap/devices.go | 121 +++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 internal/regmap/devices.go diff --git a/internal/regmap/devices.go b/internal/regmap/devices.go new file mode 100644 index 0000000..348dc6a --- /dev/null +++ b/internal/regmap/devices.go @@ -0,0 +1,121 @@ +package regmap + +import ( + "encoding/binary" + "io" + + "tinygo.org/x/drivers" +) + +// Device8 implements common logic to most 8-bit peripherals with an I2C or SPI bus. +type Device8 struct { + buf [10]byte +} + +// clear zeroes Device8's buffers. +func (d *Device8) clear() { + d.buf = [10]byte{} +} + +// I2C methods. + +func (d *Device8) Read8I2C(bus drivers.I2C, i2cAddr uint16, addr uint8) (byte, error) { + d.buf[0] = addr + err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:2]) + return d.buf[1], err +} + +func (d *Device8) Read16I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, order binary.ByteOrder) (uint16, error) { + d.buf[0] = addr + err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:3]) + return order.Uint16(d.buf[1:3]), err +} + +func (d *Device8) Read32I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, order binary.ByteOrder) (uint32, error) { + d.buf[0] = addr + err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:5]) + return order.Uint32(d.buf[1:5]), err +} + +func (d *Device8) ReadDataI2C(bus drivers.I2C, i2cAddr uint16, addr uint8, dataDestination []byte) error { + d.buf[0] = addr + return bus.Tx(i2cAddr, d.buf[:1], dataDestination) +} + +func (d *Device8) Write8I2C(bus drivers.I2C, i2cAddr uint16, addr, value uint8) error { + d.buf[0] = addr + d.buf[1] = value + return bus.Tx(i2cAddr, d.buf[:2], nil) +} + +func (d *Device8) Write16I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, value uint16, order binary.ByteOrder) error { + d.buf[0] = addr + order.PutUint16(d.buf[1:3], value) + return bus.Tx(i2cAddr, d.buf[0:3], nil) +} + +func (d *Device8) Write32I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, value uint32, order binary.ByteOrder) error { + d.buf[0] = addr + order.PutUint32(d.buf[1:5], value) + return bus.Tx(i2cAddr, d.buf[0:5], nil) +} + +// SPI methods. + +func (d *Device8) Read8SPI(bus drivers.SPI, addr uint8) (byte, error) { + d.clear() + d.buf[0] = addr + err := bus.Tx(d.buf[0:1], d.buf[1:2]) // We suppose data is returned after first byte in SPI. + return d.buf[1], err +} + +func (d *Device8) Read16SPI(bus drivers.SPI, addr uint8, order binary.ByteOrder) (uint16, error) { + d.clear() + d.buf[0] = addr + err := bus.Tx(d.buf[0:3], d.buf[3:6]) // We suppose data is returned after first byte in SPI. + return order.Uint16(d.buf[4:6]), err +} + +func (d *Device8) Read32SPI(bus drivers.SPI, addr uint8, order binary.ByteOrder) (uint32, error) { + d.clear() + d.buf[0] = addr + err := bus.Tx(d.buf[0:5], d.buf[5:10]) // We suppose data is returned after first byte in SPI. + return order.Uint32(d.buf[6:10]), err +} + +// ReadDataSPI reads data from a 8bit device address. It assumes data at register address is sent back +// from device after first byte is written as address. +// It needs the auxiliary buffer length to be large enough to contain both the write and read portions of buffer, +// so 2*(dataLength+1) < len(auxiliaryBuf) must hold. +func (d *Device8) ReadDataSPI(bus drivers.SPI, addr uint8, dataLength int, auxiliaryBuf []byte) ([]byte, error) { + split := len(auxiliaryBuf) / 2 + if split < dataLength+1 { + return nil, io.ErrShortBuffer + } + + wbuf, rbuf := auxiliaryBuf[:split], auxiliaryBuf[split:] + wbuf[0] = addr + err := bus.Tx(wbuf, rbuf) + return rbuf[1:], err +} + +func (d *Device8) Write8SPI(bus drivers.SPI, addr, value uint8) error { + d.clear() + d.buf[0] = addr + d.buf[1] = value + return bus.Tx(d.buf[:2], nil) +} + +func (d *Device8) Write16SPI(bus drivers.SPI, addr uint8, value uint16, order binary.ByteOrder) error { + d.clear() + d.buf[0] = addr + order.PutUint16(d.buf[1:3], value) + return bus.Tx(d.buf[:3], nil) +} + +func (d *Device8) Write32SPI(bus drivers.SPI, addr uint8, value uint32, order binary.ByteOrder) error { + d.clear() + d.buf[0] = addr + order.PutUint32(d.buf[1:5], value) + return bus.Tx(d.buf[:5], nil) +} From 297ad416d371925407cd16523f73bed4b43abc66 Mon Sep 17 00:00:00 2001 From: Martin Heck Date: Tue, 23 Sep 2025 08:35:08 +0200 Subject: [PATCH 085/137] fix: add RP2350 to quadrature_interrupt.go --- encoders/quadrature_interrupt.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/encoders/quadrature_interrupt.go b/encoders/quadrature_interrupt.go index 67c70bc..8a3b959 100644 --- a/encoders/quadrature_interrupt.go +++ b/encoders/quadrature_interrupt.go @@ -1,4 +1,4 @@ -//go:build tinygo && (rp2040 || stm32 || k210 || esp32c3 || nrf || sam || (avr && (atmega328p || atmega328pb))) +//go:build tinygo && (rp2040 || rp2350 || stm32 || k210 || esp32c3 || nrf || sam || (avr && (atmega328p || atmega328pb))) // Implementation based on: // https://gist.github.com/aykevl/3fc1683ed77bb0a9c07559dfe857304a From 5fb935001e8b6416a01f497ec92cf9835a5bceaa Mon Sep 17 00:00:00 2001 From: Pat Whittingslow Date: Sat, 8 Nov 2025 06:21:01 -0300 Subject: [PATCH 086/137] PinInput+PinOutput HAL (#753, reloaded) (#795) * first commit: add HAL and uc8151 driver demo * unexport drivers.PinOutput/Input HAL * fix non-tinygo pin config build * change of heart * docs: corrected some comments that were not changed at the same time as recent renaming --- internal/legacy/pinconfig.go | 62 ++++++++++++++++++++++++ internal/legacy/pinconfig_go.go | 15 ++++++ internal/legacy/pinconfig_nopulls.go | 10 ++++ internal/legacy/pinconfig_pulls.go | 13 +++++ internal/legacy/pinconfig_tinygo.go | 37 ++++++++++++++ internal/pin/internalpin.go | 72 ++++++++++++++++++++++++++++ uc8151/uc8151.go | 40 +++++++++------- 7 files changed, 232 insertions(+), 17 deletions(-) create mode 100644 internal/legacy/pinconfig.go create mode 100644 internal/legacy/pinconfig_go.go create mode 100644 internal/legacy/pinconfig_nopulls.go create mode 100644 internal/legacy/pinconfig_pulls.go create mode 100644 internal/legacy/pinconfig_tinygo.go create mode 100644 internal/pin/internalpin.go diff --git a/internal/legacy/pinconfig.go b/internal/legacy/pinconfig.go new file mode 100644 index 0000000..d9d805f --- /dev/null +++ b/internal/legacy/pinconfig.go @@ -0,0 +1,62 @@ +package legacy + +import ( + "errors" + + "tinygo.org/x/drivers/internal/pin" +) + +// The pingconfig group of files serve to abstract away +// pin configuration calls on the machine.Pin type. +// It was observed this way of developing drivers was +// non-portable and unusable on "big" Go projects so +// future projects should NOT configure pins in driver code. +// Users must configure pins before passing them as arguments +// to drivers. + +// ConfigurePinOut is a legacy function used to configure pins as outputs. +// +// Deprecated: Do not configure pins in drivers. +// This is a legacy feature and should only be used by drivers that +// previously configured pins in initialization to avoid breaking users. +func ConfigurePinOut(po pin.Output) { + configurePinOut(po) +} + +// ConfigurePinInput is a legacy function used to configure pins as inputs. +// +// Deprecated: Do not configure pins in drivers. +// This is a legacy feature and should only be used by drivers that +// previously configured pins in initialization to avoid breaking users. +func ConfigurePinInputPulldown(pi pin.Input) { + configurePinInputPulldown(pi) +} + +// ConfigurePinInput is a legacy function used to configure pins as inputs. +// +// Deprecated: Do not configure pins in drivers. +// This is a legacy feature and should only be used by drivers that +// previously configured pins in initialization to avoid breaking users. +func ConfigurePinInput(pi pin.Input) { + configurePinInput(pi) +} + +// ConfigurePinInput is a legacy function used to configure pins as inputs. +// +// Deprecated: Do not configure pins in drivers. +// This is a legacy feature and should only be used by drivers that +// previously configured pins in initialization to avoid breaking users. +func ConfigurePinInputPullup(pi pin.Input) { + configurePinInputPullup(pi) +} + +// PinIsNoPin returns true if the argument is a machine.Pin type and is the machine.NoPin predeclared type. +// +// Deprecated: Drivers do not require pin knowledge from now on. +func PinIsNoPin(pin any) bool { + return pinIsNoPin(pin) +} + +var ( + ErrConfigBeforeInstantiated = errors.New("device must be instantiated with New before calling Configure method") +) diff --git a/internal/legacy/pinconfig_go.go b/internal/legacy/pinconfig_go.go new file mode 100644 index 0000000..3a7096b --- /dev/null +++ b/internal/legacy/pinconfig_go.go @@ -0,0 +1,15 @@ +//go:build !tinygo + +package legacy + +import "tinygo.org/x/drivers/internal/pin" + +// This file compiles for non-tinygo builds +// for use with "big" or "upstream" Go where +// there is no machine package. + +func configurePinOut(p pin.Output) {} +func configurePinInput(p pin.Input) {} +func configurePinInputPulldown(p pin.Input) {} +func configurePinInputPullup(p pin.Input) {} +func pinIsNoPin(a any) bool { return false } diff --git a/internal/legacy/pinconfig_nopulls.go b/internal/legacy/pinconfig_nopulls.go new file mode 100644 index 0000000..1fc6138 --- /dev/null +++ b/internal/legacy/pinconfig_nopulls.go @@ -0,0 +1,10 @@ +//go:build baremetal && fe310 + +package legacy + +import "machine" + +const ( + pulldown = machine.PinInput + pullup = machine.PinInput +) diff --git a/internal/legacy/pinconfig_pulls.go b/internal/legacy/pinconfig_pulls.go new file mode 100644 index 0000000..d7e88c6 --- /dev/null +++ b/internal/legacy/pinconfig_pulls.go @@ -0,0 +1,13 @@ +//go:build baremetal && !fe310 + +package legacy + +import "machine" + +// If you are getting a build error here you then we missed adding +// your CPU build tag to the list of CPUs that do not have pulldown/pullups. +// Add it above and in pinhal_nopulls! You should also add a smoketest for it :) +const ( + pulldown = machine.PinInputPulldown + pullup = machine.PinInputPullup +) diff --git a/internal/legacy/pinconfig_tinygo.go b/internal/legacy/pinconfig_tinygo.go new file mode 100644 index 0000000..d0068b2 --- /dev/null +++ b/internal/legacy/pinconfig_tinygo.go @@ -0,0 +1,37 @@ +//go:build baremetal + +package legacy + +import ( + "machine" + + "tinygo.org/x/drivers/internal/pin" +) + +func configurePinOut(po pin.Output) { + configurePin(po, machine.PinOutput) +} + +func configurePinInputPulldown(pi pin.Input) { + configurePin(pi, pulldown) // some chips do not have pull down, in which case pulldown==machine.PinInput. +} + +func configurePinInput(pi pin.Input) { + configurePin(pi, machine.PinInput) +} + +func configurePinInputPullup(pi pin.Input) { + configurePin(pi, pullup) // some chips do not have pull up, in which case pullup==machine.PinInput. +} + +func pinIsNoPin(a any) bool { + p, ok := a.(machine.Pin) + return ok && p == machine.NoPin +} + +func configurePin(p any, mode machine.PinMode) { + machinePin, ok := p.(machine.Pin) + if ok { + machinePin.Configure(machine.PinConfig{Mode: mode}) + } +} diff --git a/internal/pin/internalpin.go b/internal/pin/internalpin.go new file mode 100644 index 0000000..6879f34 --- /dev/null +++ b/internal/pin/internalpin.go @@ -0,0 +1,72 @@ +// package pin implements a TinyGo Pin HAL. +// It serves to eliminate machine.Pin from driver constructors +// so that drivers can be used in "big" Go projects where +// there is no machine package. +// This file contains both function and interface-style Pin HAL definitions. +package pin + +// OutputFunc is hardware abstraction for a pin which outputs a +// digital signal (high or low level). +// +// // Code conversion demo: from machine.Pin to pin.OutputFunc +// led := machine.LED +// led.Configure(machine.PinConfig{Mode: machine.Output}) +// var pin pin.OutputFunc = led.Set // Going from a machine.Pin to a pin.OutputFunc +// +// This is an alternative to [Output] which is an interface type. +type OutputFunc func(level bool) + +// High sets the underlying pin's level to high. This is equivalent to calling PinOutput(true). +func (setPin OutputFunc) High() { + setPin(true) +} + +// Low sets the underlying pin's level to low. This is equivalent to calling PinOutput(false). +func (setPin OutputFunc) Low() { + setPin(false) +} + +// InputFunc is hardware abstraction for a pin which receives a +// digital signal and reads it (high or low level). +// +// // Code conversion demo: from machine.Pin to pin.InputFunc +// input := machine.LED +// input.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) // or use machine.PinInputPullup or machine.Input +// var pin pin.InputFunc = input.Get // Going from a machine.Pin to a pin.InputFunc +// +// This is an alternative to [Input] which is an interface type. +type InputFunc func() (level bool) + +// // Below is an example on how to define a input/output pin HAL for a +// // pin that must switch between input and output mode: +// +// var pinIsOutput bool +// var po PinOutputFunc = func(b bool) { +// if !pinIsOutput { +// pin.Configure(outputMode) +// pinIsOutput = true +// } +// pin.Set(b) +// } +// +// var pi PinInputFunc = func() bool { +// if pinIsOutput { +// pin.Configure(inputMode) +// pinIsOutput = false +// } +// return pin.Get() +// } + +// Output interface represents a pin hardware abstraction layer for a pin that can output a digital signal. +// +// This is an alternative to [OutputFunc] abstraction which is a function type. +type Output interface { + Set(level bool) +} + +// Input interface represents a pin hardware abstraction layer for a pin that can read a digital signal. +// +// This is an alternative to [InputFunc] abstraction which is a function type. +type Input interface { + Get() (level bool) +} diff --git a/uc8151/uc8151.go b/uc8151/uc8151.go index 38c48f9..4367568 100644 --- a/uc8151/uc8151.go +++ b/uc8151/uc8151.go @@ -8,10 +8,11 @@ package uc8151 // import "tinygo.org/x/drivers/uc8151" import ( "errors" "image/color" - "machine" "time" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/internal/pin" "tinygo.org/x/drivers/pixel" ) @@ -31,10 +32,10 @@ type Config struct { type Device struct { bus drivers.SPI - cs machine.Pin - dc machine.Pin - rst machine.Pin - busy machine.Pin + cs pin.OutputFunc + dc pin.OutputFunc + rst pin.OutputFunc + isBusy pin.InputFunc width int16 height int16 buffer []uint8 @@ -49,17 +50,22 @@ type Device struct { type Speed uint8 // New returns a new uc8151 driver. Pass in a fully configured SPI bus. -func New(bus drivers.SPI, csPin, dcPin, rstPin, busyPin machine.Pin) Device { - csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - rstPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - busyPin.Configure(machine.PinConfig{Mode: machine.PinInput}) +// Pins passed in must be configured beforehand. +func New(bus drivers.SPI, csPin, dcPin, rstPin pin.Output, busyPin pin.Input) Device { + // For backwards compatibility. + // This driver used to configure pins, + // so leave in to not break users. + // May be removed in future so try not to depend on it! + legacy.ConfigurePinOut(csPin) + legacy.ConfigurePinOut(dcPin) + legacy.ConfigurePinOut(rstPin) + legacy.ConfigurePinInput(busyPin) return Device{ - bus: bus, - cs: csPin, - dc: dcPin, - rst: rstPin, - busy: busyPin, + bus: bus, + cs: csPin.Set, + dc: dcPin.Set, + rst: rstPin.Set, + isBusy: busyPin.Get, } } @@ -313,14 +319,14 @@ func (d *Device) ClearDisplay() { // WaitUntilIdle waits until the display is ready func (d *Device) WaitUntilIdle() { - for !d.busy.Get() { + for !d.isBusy() { time.Sleep(10 * time.Millisecond) } } // IsBusy returns the busy status of the display func (d *Device) IsBusy() bool { - return d.busy.Get() + return d.isBusy() } // ClearBuffer sets the buffer to 0xFF (white) From ec680be7842f68659cd0f007ca83bf3e0269bc59 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sat, 11 Oct 2025 09:32:41 +0200 Subject: [PATCH 087/137] lis3dh: use correct error handling and make configurable Instead of printing an error, this driver really should be returning errors instead. Also, `Configure` didn't have a way to actually configure the driver. This is now added, and can be expanded in the future. This is a breaking change. --- examples/lis3dh/main.go | 15 +++++++-- lis3dh/lis3dh.go | 68 ++++++++++++++++++++++++++++------------- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/examples/lis3dh/main.go b/examples/lis3dh/main.go index cab970e..6ca11c9 100644 --- a/examples/lis3dh/main.go +++ b/examples/lis3dh/main.go @@ -14,9 +14,18 @@ func main() { i2c.Configure(machine.I2CConfig{SCL: machine.SCL1_PIN, SDA: machine.SDA1_PIN}) accel := lis3dh.New(i2c) - accel.Address = lis3dh.Address1 // address on the Circuit Playground Express - accel.Configure() - accel.SetRange(lis3dh.RANGE_2_G) + err := accel.Configure(lis3dh.Config{ + Address: lis3dh.Address1, // address on the Circuit Playground Express + }) + for err != nil { + println("could not configure LIS3DH:", err) + time.Sleep(time.Second) + } + err = accel.SetRange(lis3dh.RANGE_2_G) + for err != nil { + println("could not set acceleration range:", err) + time.Sleep(time.Second) + } println(accel.Connected()) diff --git a/lis3dh/lis3dh.go b/lis3dh/lis3dh.go index 67f3200..9de6c4e 100644 --- a/lis3dh/lis3dh.go +++ b/lis3dh/lis3dh.go @@ -11,37 +11,56 @@ import ( // Device wraps an I2C connection to a LIS3DH device. type Device struct { bus drivers.I2C - Address uint16 + address uint16 r Range } +// Driver configuration, used for the Configure call. All fields are optional. +type Config struct { + Address uint16 +} + // New creates a new LIS3DH connection. The I2C bus must already be configured. // // This function only creates the Device object, it does not touch the device. func New(bus drivers.I2C) Device { - return Device{bus: bus, Address: Address0} + return Device{bus: bus, address: Address0} } // Configure sets up the device for communication -func (d *Device) Configure() { +func (d *Device) Configure(config Config) error { + if config.Address != 0 { + d.address = config.Address + } + // enable all axes, normal mode - legacy.WriteRegister(d.bus, uint8(d.Address), REG_CTRL1, []byte{0x07}) + err := legacy.WriteRegister(d.bus, uint8(d.address), REG_CTRL1, []byte{0x07}) + if err != nil { + return err + } // 400Hz rate - d.SetDataRate(DATARATE_400_HZ) + err = d.SetDataRate(DATARATE_400_HZ) + if err != nil { + return err + } // High res & BDU enabled - legacy.WriteRegister(d.bus, uint8(d.Address), REG_CTRL4, []byte{0x88}) + err = legacy.WriteRegister(d.bus, uint8(d.address), REG_CTRL4, []byte{0x88}) + if err != nil { + return err + } // get current range - d.r = d.ReadRange() + d.r, err = d.ReadRange() + return err } // Connected returns whether a LIS3DH has been found. // It does a "who am I" request and checks the response. func (d *Device) Connected() bool { data := []byte{0} - err := legacy.ReadRegister(d.bus, uint8(d.Address), WHO_AM_I, data) + err := legacy.ReadRegister(d.bus, uint8(d.address), WHO_AM_I, data) if err != nil { return false } @@ -49,46 +68,51 @@ func (d *Device) Connected() bool { } // SetDataRate sets the speed of data collected by the LIS3DH. -func (d *Device) SetDataRate(rate DataRate) { +func (d *Device) SetDataRate(rate DataRate) error { ctl1 := []byte{0} - err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CTRL1, ctl1) + err := legacy.ReadRegister(d.bus, uint8(d.address), REG_CTRL1, ctl1) if err != nil { - println(err.Error()) + return err } // mask off bits ctl1[0] &^= 0xf0 ctl1[0] |= (byte(rate) << 4) - legacy.WriteRegister(d.bus, uint8(d.Address), REG_CTRL1, ctl1) + return legacy.WriteRegister(d.bus, uint8(d.address), REG_CTRL1, ctl1) } // SetRange sets the G range for LIS3DH. -func (d *Device) SetRange(r Range) { +func (d *Device) SetRange(r Range) error { ctl := []byte{0} - err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CTRL4, ctl) + err := legacy.ReadRegister(d.bus, uint8(d.address), REG_CTRL4, ctl) if err != nil { - println(err.Error()) + return err } // mask off bits ctl[0] &^= 0x30 ctl[0] |= (byte(r) << 4) - legacy.WriteRegister(d.bus, uint8(d.Address), REG_CTRL4, ctl) + err = legacy.WriteRegister(d.bus, uint8(d.address), REG_CTRL4, ctl) + if err != nil { + return err + } // store the new range d.r = r + + return nil } // ReadRange returns the current G range for LIS3DH. -func (d *Device) ReadRange() (r Range) { +func (d *Device) ReadRange() (r Range, err error) { ctl := []byte{0} - err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CTRL4, ctl) + err = legacy.ReadRegister(d.bus, uint8(d.address), REG_CTRL4, ctl) if err != nil { - println(err.Error()) + return 0, err } // mask off bits r = Range(ctl[0] >> 4) r &= 0x03 - return r + return r, nil } // ReadAcceleration reads the current acceleration from the device and returns @@ -114,10 +138,10 @@ func (d *Device) ReadAcceleration() (int32, int32, int32, error) { // ReadRawAcceleration returns the raw x, y and z axis from the LIS3DH func (d *Device) ReadRawAcceleration() (x int16, y int16, z int16) { - legacy.WriteRegister(d.bus, uint8(d.Address), REG_OUT_X_L|0x80, nil) + legacy.WriteRegister(d.bus, uint8(d.address), REG_OUT_X_L|0x80, nil) data := []byte{0, 0, 0, 0, 0, 0} - d.bus.Tx(d.Address, nil, data) + d.bus.Tx(d.address, nil, data) x = int16((uint16(data[1]) << 8) | uint16(data[0])) y = int16((uint16(data[3]) << 8) | uint16(data[2])) From 51b604ce97ffece5e5a43902305dad681b162de5 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sat, 11 Oct 2025 10:15:27 +0200 Subject: [PATCH 088/137] lis3dh: add Update and Acceleration calls This adjusts the API to the one proposed in https://github.com/tinygo-org/drivers/pull/345, which I think is much better than direct ReadAcceleration etc calls. I have also updated the code that converts raw acceleration values to normalized values. The new code should be faster (didn't measure) and avoids floating point math. --- lis3dh/lis3dh.go | 83 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 14 deletions(-) diff --git a/lis3dh/lis3dh.go b/lis3dh/lis3dh.go index 9de6c4e..0151e08 100644 --- a/lis3dh/lis3dh.go +++ b/lis3dh/lis3dh.go @@ -13,6 +13,7 @@ type Device struct { bus drivers.I2C address uint16 r Range + accel [6]byte // stored acceleration data (from the Update call) } // Driver configuration, used for the Configure call. All fields are optional. @@ -120,20 +121,9 @@ func (d *Device) ReadRange() (r Range, err error) { // and the sensor is not moving the returned value will be around 1000000 or // -1000000. func (d *Device) ReadAcceleration() (int32, int32, int32, error) { - x, y, z := d.ReadRawAcceleration() - divider := float32(1) - switch d.r { - case RANGE_16_G: - divider = 1365 - case RANGE_8_G: - divider = 4096 - case RANGE_4_G: - divider = 8190 - case RANGE_2_G: - divider = 16380 - } - - return int32(float32(x) / divider * 1000000), int32(float32(y) / divider * 1000000), int32(float32(z) / divider * 1000000), nil + rawX, rawY, rawZ := d.ReadRawAcceleration() + x, y, z := normalizeRange(rawX, rawY, rawZ, d.r) + return x, y, z, nil } // ReadRawAcceleration returns the raw x, y and z axis from the LIS3DH @@ -149,3 +139,68 @@ func (d *Device) ReadRawAcceleration() (x int16, y int16, z int16) { return } + +// Update the sensor values of the 'which' parameter. Only acceleration is +// supported at the moment. +func (d *Device) Update(which drivers.Measurement) error { + if which&drivers.Acceleration != 0 { + // Read raw acceleration values and store them in the driver. + err := legacy.WriteRegister(d.bus, uint8(d.address), REG_OUT_X_L|0x80, nil) + if err != nil { + return err + } + err = d.bus.Tx(d.address, nil, d.accel[:]) + if err != nil { + return err + } + } + return nil +} + +// Acceleration returns the last read acceleration in µg (micro-gravity). +// When one of the axes is pointing straight to Earth and the sensor is not +// moving the returned value will be around 1000000 or -1000000. +func (d *Device) Acceleration() (x, y, z int32) { + // Extract the raw 16-bit values. + rawX := int16((uint16(d.accel[1]) << 8) | uint16(d.accel[0])) + rawY := int16((uint16(d.accel[3]) << 8) | uint16(d.accel[2])) + rawZ := int16((uint16(d.accel[5]) << 8) | uint16(d.accel[4])) + + // Normalize these values, to be in µg (micro-gravity). + return normalizeRange(rawX, rawY, rawZ, d.r) +} + +// Convert raw 16-bit values to normalized 32-bit values while avoiding floats +// and divisions. +func normalizeRange(rawX, rawY, rawZ int16, r Range) (x, y, z int32) { + // We're going to convert the 16-bit raw values to values in the range + // -1000_000..1000_000. For now we're going to assume a range of 16G, we'll + // adjust that range later. + // The formula is derived as follows, and carefully selected to avoid + // overflow and integer divisions (the division will be optimized to a + // bitshift): + // x = x * 1000_000 / 2048 + // x = x * (1000_000/64) / (2048/64) + // x = x * 15625 / 32 + x = int32(rawX) * 15625 / 32 + y = int32(rawY) * 15625 / 32 + z = int32(rawZ) * 15625 / 32 + + // Now we need to normalize the three values, since we assumed 16G before. + shift := uint32(0) + switch r { + case RANGE_16_G: + shift = 0 + case RANGE_8_G: + shift = 1 + case RANGE_4_G: + shift = 2 + case RANGE_2_G: + shift = 3 + } + x >>= shift + y >>= shift + z >>= shift + + return +} From 5cb360a4bfc2c18c9af8cfedb3251a7dbfb1a309 Mon Sep 17 00:00:00 2001 From: Pat Whittingslow Date: Sat, 8 Nov 2025 06:45:18 -0300 Subject: [PATCH 089/137] Add Honeywell HSC TruStability SPI+I2C pressure sensor driver (#799) * add honeywell pressure sensor * apply @aykevl suggestions --- examples/honeyhsc/example.go | 49 +++++++++ honeyhsc/hsc.go | 191 +++++++++++++++++++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 examples/honeyhsc/example.go create mode 100644 honeyhsc/hsc.go diff --git a/examples/honeyhsc/example.go b/examples/honeyhsc/example.go new file mode 100644 index 0000000..1f34d21 --- /dev/null +++ b/examples/honeyhsc/example.go @@ -0,0 +1,49 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/honeyhsc" +) + +// Data taken from https://github.com/rodan/honeywell_hsc_ssc_i2c/blob/master/hsc_ssc_i2c.cpp +// these defaults are valid for the HSCMRNN030PA2A3 chip +const ( + i2cAddress = 0x28 + // 10% + outputMinimum = 0x666 + // 90% of 2^14 - 1 + outputMax = 0x399A + // min is 0 for sensors that give absolute values + pressureMin = 0 + // 30psi (and we want results in millipascals) + // pressureMax = 206842.7 + pressureMax = 206843 * 1000 +) + +func main() { + bus := machine.I2C0 + err := bus.Configure(machine.I2CConfig{ + Frequency: 400_000, // 100kHz minimum and 400kHz I2C maximum clock. 50 to 800 for SPI. + SDA: machine.I2C0_SDA_PIN, + SCL: machine.I2C0_SCL_PIN, + }) + if err != nil { + panic(err.Error()) + } + sensor := honeyhsc.NewDevI2C(bus, i2cAddress, outputMinimum, outputMax, pressureMin, pressureMax) + for { + time.Sleep(time.Second) + const measuremask = drivers.Pressure | drivers.Temperature + err := sensor.Update(measuremask) + if err != nil { + println("error updating measurements:", err.Error()) + continue + } + P := sensor.Pressure() + T := sensor.Temperature() + println("pressure:", P, "temperature:", T) + } +} diff --git a/honeyhsc/hsc.go b/honeyhsc/hsc.go new file mode 100644 index 0000000..efc58a5 --- /dev/null +++ b/honeyhsc/hsc.go @@ -0,0 +1,191 @@ +package honeyhsc + +import ( + "errors" + "math" + + "tinygo.org/x/drivers" +) + +var ( + errSensorMissing = errors.New("hsc: not connected") + errDiagnostic = errors.New("hsc: diagnostic error") +) + +const ( + measuremask = drivers.Pressure | drivers.Temperature + statusMask = 0b1100_0000 + statusOffset = 6 +) + +// DevI2C is the TruStability® High Accuracy Silicon Ceramic (HSC) Series is a piezoresistive silicon pressure sensor offering a ratiometric +// analog or digital output for reading pressure over the specified full scale pressure span and temperature range. +type DevI2C struct { + bus drivers.I2C + dev + addr uint8 + buf [6]byte +} + +// NewDevI2C creates and returns a new DevI2C that communicates with an HSC device over the provided I2C bus. +// Parameters: +// - bus: the I2C bus to use. +// - addr: the 7-bit I2C address of the sensor. +// - outMin, outMax: raw output code range (counts) corresponding to the pressure span. Depends on sensor model. +// - pMin, pMax: pressure range endpoints in millipascals (mPa). Depends on sensor model. +// +// The returned DevI2C will use these calibration parameters to convert raw bridge counts to pressure. +func NewDevI2C(bus drivers.I2C, addr, outMin, outMax uint16, pMin, pMax int32) *DevI2C { + h := &DevI2C{ + bus: bus, + addr: uint8(addr), + dev: dev{ + cmin: outMin, + cmax: outMax, + pmin: pMin, + pmax: pMax, + }, + } + return h +} + +// ReadTemperature reads and returns the temperature in milliKelvin (mC) from the I2C-attached HSC device. +// It performs an Update internally to get the latest temperature value. +func (h *DevI2C) ReadTemperature() (int32, error) { + err := h.Update(drivers.Temperature) + if err != nil { + return 0, err + } + return h.Temperature(), nil +} + +// Update reads both temperature and pressure data from the I2C-attached HSC device when +// the requested measurement mask includes pressure or temperature. +// If neither pressure nor temperature is requested, Update is a no-op. +func (d *DevI2C) Update(which drivers.Measurement) error { + // Update performs an I2C transaction to read 4 bytes, parses the status bits, 14-bit bridge data and + // temperature bits, and forwards them to the internal update routine. Any I2C transport error is returned, + // as well as errors produced by the internal update (e.g. errSensorMissing, errDiagnostic). + if which&measuremask == 0 { + return nil + } + rbuf := d.buf[:4] + wbuf := d.buf[4:6] + const reg = 0 + value := (d.addr << 1) | 1 + wbuf[0] = reg + wbuf[1] = value + err := d.bus.Tx(uint16(d.addr), wbuf, rbuf) + if err != nil { + return err + } + status := (rbuf[0] & statusMask) >> statusOffset + bridgeData := (uint16(rbuf[0]&^statusMask) << 8) | uint16(rbuf[1]) + tempData := uint16(rbuf[2])<<8 | uint16(rbuf[3]&0xe0)>>5 + return d.dev.update(status, bridgeData, tempData) +} + +type pinout func(level bool) + +// DevI2C is the TruStability® High Accuracy Silicon Ceramic (HSC) Series is a piezoresistive silicon pressure sensor offering a ratiometric +// analog or digital output for reading pressure over the specified full scale pressure span and temperature range. +type DevSPI struct { + spi drivers.SPI + cs pinout + dev + buf [4]byte +} + +// NewDevSPI creates and returns a new DevSPI that communicates with an HSC device over SPI. +// Parameters: +// - conn: the SPI connection to use. +// - cs: a chip-select function that drives the device select line low/high. +// - outMin, outMax: raw output code range (counts) corresponding to the pressure span. Depends on sensor model. +// - pMin, pMax: pressure range endpoints in millipascals (mPa). Depends on sensor model. +// +// The function returns the constructed DevSPI and an error value (currently always nil). +func NewDevSPI(conn drivers.SPI, cs pinout, outMin, outMax uint16, pMin, pMax int32) (*DevSPI, error) { + h := &DevSPI{ + spi: conn, + cs: cs, + dev: dev{ + cmin: outMin, + cmax: outMax, + pmin: pMin, + pmax: pMax, + }, + } + return h, nil +} + +// ReadTemperature reads and returns the temperature in milliKelvin (mC) from the SPI-attached HSC device. +// It performs an Update internally to get the latest temperature value. +func (h *DevSPI) ReadTemperature() (int32, error) { + err := h.Update(drivers.Temperature) + if err != nil { + return 0, err + } + return h.Temperature(), nil +} + +// Update reads pressure and temperature data from the SPI-attached HSC device when the requested measurement mask includes +// pressure or temperature. If neither pressure nor temperature is requested, Update is a no-op. +func (h *DevSPI) Update(which drivers.Measurement) error { + // It toggles the provided chip-select, performs an SPI transfer to read 4 bytes, parses the status bits, + // 14-bit bridge data and temperature bits, and forwards them to the internal update routine. Any SPI + // transport error is returned, as well as errors produced by the internal update (e.g. errSensorMissing, errDiagnostic). + if which&measuremask == 0 { + return nil + } + buf := &h.buf + h.cs(false) + err := h.spi.Tx(nil, buf[:4]) + h.cs(true) + if err != nil { + return err + } + // First two bits are status bits. + status := (buf[0] & statusMask) >> statusOffset + bridgeData := (uint16(buf[0]&^statusMask) << 8) | uint16(buf[1]) + + tempData := uint16(buf[2])<<8 | uint16(buf[3]&0xe0)>>5 + return h.dev.update(status, bridgeData, tempData) +} + +type dev struct { + pressure int32 + temp int32 + cmin, cmax uint16 + pmin, pmax int32 +} + +// Pressure returns the most recently computed pressure value in millipascals (mPa). +// The value is taken from the last successful Update. +func (d *dev) Pressure() int32 { + return d.pressure +} + +// Temperature returns the most recently read temperature value in milliKelvin (mC). +// The value is taken from the last successful Update. +func (d *dev) Temperature() int32 { + return d.temp + 273_150 +} + +// update interprets raw sensor fields (status, bridgeData, tempData) and updates the dev's stored +// pressure and temperature. It returns errSensorMissing when the temperature raw value indicates no sensor +// (tempData == math.MaxUint16), errDiagnostic when the status indicates a device diagnostic condition +// (status == 3), or nil on success. Pressure is computed with integer arithmetic using the configured +// cmin/cmax -> pmin/pmax linear mapping in order to avoid overflows. +func (d *dev) update(status uint8, bridgeData, tempData uint16) error { + if tempData == math.MaxUint16 { + return errSensorMissing + } else if status == 3 { + return errDiagnostic + } + + // Take care not to overflow here. + p := (int32(bridgeData)-int32(d.cmin))*(d.pmax-d.pmin)/int32(d.cmax-d.cmin) + d.pmin + d.temp = int32(tempData) + d.pressure = p + return nil +} From 34da2d208ad2e19cfceb9b21270e9107065a2ecc Mon Sep 17 00:00:00 2001 From: Yurii Soldak Date: Fri, 17 Oct 2025 23:49:21 +0200 Subject: [PATCH 090/137] lsm9ds1: avoid unnecessary heap allocations --- lsm9ds1/lsm9ds1.go | 63 +++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/lsm9ds1/lsm9ds1.go b/lsm9ds1/lsm9ds1.go index e77029e..cae3bcf 100644 --- a/lsm9ds1/lsm9ds1.go +++ b/lsm9ds1/lsm9ds1.go @@ -7,7 +7,6 @@ import ( "errors" "tinygo.org/x/drivers" - "tinygo.org/x/drivers/internal/legacy" ) type AccelRange uint8 @@ -28,7 +27,7 @@ type Device struct { accelMultiplier int32 gyroMultiplier int32 magMultiplier int32 - buf [6]uint8 + buf [7]uint8 // up to 6 bytes for read + 1 byte for the register address } // Configuration for LSM9DS1 device. @@ -61,10 +60,15 @@ func New(bus drivers.I2C) *Device { // Case of boolean false and error nil means I2C is up, // but "who am I" responses have unexpected values. func (d *Device) Connected() bool { - data1, data2 := d.buf[:1], d.buf[1:2] - legacy.ReadRegister(d.bus, d.AccelAddress, WHO_AM_I, data1) - legacy.ReadRegister(d.bus, d.MagAddress, WHO_AM_I_M, data2) - return data1[0] == 0x68 && data2[0] == 0x3D + data, err := d.readBytes(d.AccelAddress, WHO_AM_I, 1) + if err != nil || data[0] != 0x68 { + return false + } + data, err = d.readBytes(d.MagAddress, WHO_AM_I_M, 1) + if err != nil || data[0] != 0x3D { + return false + } + return true } // ReadAcceleration reads the current acceleration from the device and returns @@ -72,8 +76,7 @@ func (d *Device) Connected() bool { // and the sensor is not moving the returned value will be around 1000000 or // -1000000. func (d *Device) ReadAcceleration() (x, y, z int32, err error) { - data := d.buf[:6] - err = legacy.ReadRegister(d.bus, uint8(d.AccelAddress), OUT_X_L_XL, data) + data, err := d.readBytes(d.AccelAddress, OUT_X_L_XL, 6) if err != nil { return } @@ -88,8 +91,7 @@ func (d *Device) ReadAcceleration() (x, y, z int32, err error) { // rotation along one axis and while doing so integrate all values over time, // you would get a value close to 360000000. func (d *Device) ReadRotation() (x, y, z int32, err error) { - data := d.buf[:6] - err = legacy.ReadRegister(d.bus, uint8(d.AccelAddress), OUT_X_L_G, data) + data, err := d.readBytes(d.AccelAddress, OUT_X_L_G, 6) if err != nil { return } @@ -102,8 +104,7 @@ func (d *Device) ReadRotation() (x, y, z int32, err error) { // ReadMagneticField reads the current magnetic field from the device and returns // it in nT (nanotesla). 1 G (gauss) = 100_000 nT (nanotesla). func (d *Device) ReadMagneticField() (x, y, z int32, err error) { - data := d.buf[:6] - err = legacy.ReadRegister(d.bus, uint8(d.MagAddress), OUT_X_L_M, data) + data, err := d.readBytes(d.MagAddress, OUT_X_L_M, 6) if err != nil { return } @@ -115,8 +116,7 @@ func (d *Device) ReadMagneticField() (x, y, z int32, err error) { // ReadTemperature returns the temperature in Celsius milli degrees (°C/1000) func (d *Device) ReadTemperature() (t int32, err error) { - data := d.buf[:2] - err = legacy.ReadRegister(d.bus, uint8(d.AccelAddress), OUT_TEMP_L, data) + data, err := d.readBytes(d.AccelAddress, OUT_TEMP_L, 2) if err != nil { return } @@ -167,20 +167,16 @@ func (d *Device) doConfigure(cfg Configuration) (err error) { d.magMultiplier = 58 } - data := d.buf[:1] - // Configure accelerometer // Sample rate & measurement range - data[0] = uint8(cfg.AccelSampleRate)<<5 | uint8(cfg.AccelRange)<<3 - err = legacy.WriteRegister(d.bus, d.AccelAddress, CTRL_REG6_XL, data) + err = d.writeByte(d.AccelAddress, CTRL_REG6_XL, uint8(cfg.AccelSampleRate)<<5|uint8(cfg.AccelRange)<<3) if err != nil { return } // Configure gyroscope // Sample rate & measurement range - data[0] = uint8(cfg.GyroSampleRate)<<5 | uint8(cfg.GyroRange)<<3 - err = legacy.WriteRegister(d.bus, d.AccelAddress, CTRL_REG1_G, data) + err = d.writeByte(d.AccelAddress, CTRL_REG1_G, uint8(cfg.GyroSampleRate)<<5|uint8(cfg.GyroRange)<<3) if err != nil { return } @@ -190,33 +186,44 @@ func (d *Device) doConfigure(cfg Configuration) (err error) { // Temperature compensation enabled // High-performance mode XY axis // Sample rate - data[0] = 0b10000000 | 0b01000000 | uint8(cfg.MagSampleRate)<<2 - err = legacy.WriteRegister(d.bus, d.MagAddress, CTRL_REG1_M, data) + err = d.writeByte(d.MagAddress, CTRL_REG1_M, 0b10000000|0b01000000|uint8(cfg.MagSampleRate)<<2) if err != nil { return } // Measurement range - data[0] = uint8(cfg.MagRange) << 5 - err = legacy.WriteRegister(d.bus, d.MagAddress, CTRL_REG2_M, data) + err = d.writeByte(d.MagAddress, CTRL_REG2_M, uint8(cfg.MagRange)<<5) if err != nil { return } // Continuous-conversion mode // https://electronics.stackexchange.com/questions/237397/continuous-conversion-vs-single-conversion-mode - data[0] = 0b00000000 - err = legacy.WriteRegister(d.bus, d.MagAddress, CTRL_REG3_M, data) + err = d.writeByte(d.MagAddress, CTRL_REG3_M, 0b00000000) if err != nil { return } // High-performance mode Z axis - data[0] = 0b00001000 - err = legacy.WriteRegister(d.bus, d.MagAddress, CTRL_REG4_M, data) + err = d.writeByte(d.MagAddress, CTRL_REG4_M, 0b00001000) if err != nil { return } return nil } + +func (d *Device) readBytes(addr, reg, size uint8) ([]byte, error) { + d.buf[0] = reg + err := d.bus.Tx(uint16(addr), d.buf[0:1], d.buf[1:size+1]) + if err != nil { + return nil, err + } + return d.buf[1 : size+1], nil +} + +func (d *Device) writeByte(addr, reg, value uint8) error { + d.buf[0] = reg + d.buf[1] = value + return d.bus.Tx(uint16(addr), d.buf[0:2], nil) +} From bd88b70511df2037a65ef4db1ed858c9ab67beab Mon Sep 17 00:00:00 2001 From: Pat Whittingslow Date: Sun, 9 Nov 2025 10:05:30 -0300 Subject: [PATCH 091/137] regmap: Add Device8I2C/SPI types and their logic (#801) * add regmap Device8I2C/SPI types and their methods * add endianess hint --- internal/regmap/{devices.go => device8.go} | 22 ++++ internal/regmap/i2c-spi.go | 123 +++++++++++++++++++++ 2 files changed, 145 insertions(+) rename internal/regmap/{devices.go => device8.go} (66%) create mode 100644 internal/regmap/i2c-spi.go diff --git a/internal/regmap/devices.go b/internal/regmap/device8.go similarity index 66% rename from internal/regmap/devices.go rename to internal/regmap/device8.go index 348dc6a..b62aec1 100644 --- a/internal/regmap/devices.go +++ b/internal/regmap/device8.go @@ -8,6 +8,10 @@ import ( ) // Device8 implements common logic to most 8-bit peripherals with an I2C or SPI bus. +// All methods expect the target to support conventional register read and write operations +// where the first byte sent is the register address being accessed. +// +// All methods use an internal buffer and perform no dynamic memory allocation. type Device8 struct { buf [10]byte } @@ -19,41 +23,53 @@ func (d *Device8) clear() { // I2C methods. +// Read8I2C reads a single byte from register addr of the device at i2cAddr using the provided I2C bus. func (d *Device8) Read8I2C(bus drivers.I2C, i2cAddr uint16, addr uint8) (byte, error) { d.buf[0] = addr err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:2]) return d.buf[1], err } +// Read16I2C reads a 16-bit value from register addr of the device at i2cAddr using the provided I2C bus. +// The byte order is specified by order. func (d *Device8) Read16I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, order binary.ByteOrder) (uint16, error) { d.buf[0] = addr err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:3]) return order.Uint16(d.buf[1:3]), err } +// Read32I2C reads a 32-bit value from register addr of the device at i2cAddr using the provided I2C bus. +// The byte order is specified by order. func (d *Device8) Read32I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, order binary.ByteOrder) (uint32, error) { d.buf[0] = addr err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:5]) return order.Uint32(d.buf[1:5]), err } +// ReadDataI2C reads dataLength bytes from register addr of the device at i2cAddr using the provided I2C bus. +// The data is stored in dataDestination. func (d *Device8) ReadDataI2C(bus drivers.I2C, i2cAddr uint16, addr uint8, dataDestination []byte) error { d.buf[0] = addr return bus.Tx(i2cAddr, d.buf[:1], dataDestination) } +// Write8I2C writes a single byte value to register addr of the device at i2cAddr using the provided I2C bus. func (d *Device8) Write8I2C(bus drivers.I2C, i2cAddr uint16, addr, value uint8) error { d.buf[0] = addr d.buf[1] = value return bus.Tx(i2cAddr, d.buf[:2], nil) } +// Write16I2C writes a 16-bit value to register addr of the device at i2cAddr using the provided I2C bus. +// The byte order is specified by order. func (d *Device8) Write16I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, value uint16, order binary.ByteOrder) error { d.buf[0] = addr order.PutUint16(d.buf[1:3], value) return bus.Tx(i2cAddr, d.buf[0:3], nil) } +// Write32I2C writes a 32-bit value to register addr of the device at i2cAddr using the provided I2C bus. +// The byte order is specified by order. func (d *Device8) Write32I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, value uint32, order binary.ByteOrder) error { d.buf[0] = addr order.PutUint32(d.buf[1:5], value) @@ -62,6 +78,7 @@ func (d *Device8) Write32I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, value // SPI methods. +// Read8SPI reads a single byte from register addr using the provided SPI bus. func (d *Device8) Read8SPI(bus drivers.SPI, addr uint8) (byte, error) { d.clear() d.buf[0] = addr @@ -69,6 +86,7 @@ func (d *Device8) Read8SPI(bus drivers.SPI, addr uint8) (byte, error) { return d.buf[1], err } +// Read16SPI reads a 16-bit value from register addr using the provided SPI bus. The byte order is specified by order. func (d *Device8) Read16SPI(bus drivers.SPI, addr uint8, order binary.ByteOrder) (uint16, error) { d.clear() d.buf[0] = addr @@ -76,6 +94,7 @@ func (d *Device8) Read16SPI(bus drivers.SPI, addr uint8, order binary.ByteOrder) return order.Uint16(d.buf[4:6]), err } +// Read32SPI reads a 32-bit value from register addr using the provided SPI bus. The byte order is specified by order. func (d *Device8) Read32SPI(bus drivers.SPI, addr uint8, order binary.ByteOrder) (uint32, error) { d.clear() d.buf[0] = addr @@ -99,6 +118,7 @@ func (d *Device8) ReadDataSPI(bus drivers.SPI, addr uint8, dataLength int, auxil return rbuf[1:], err } +// Write8SPI writes a single byte value to register addr using the provided SPI bus. func (d *Device8) Write8SPI(bus drivers.SPI, addr, value uint8) error { d.clear() d.buf[0] = addr @@ -106,6 +126,7 @@ func (d *Device8) Write8SPI(bus drivers.SPI, addr, value uint8) error { return bus.Tx(d.buf[:2], nil) } +// Write16SPI writes a 16-bit value to register addr using the provided SPI bus. The byte order is specified by order. func (d *Device8) Write16SPI(bus drivers.SPI, addr uint8, value uint16, order binary.ByteOrder) error { d.clear() d.buf[0] = addr @@ -113,6 +134,7 @@ func (d *Device8) Write16SPI(bus drivers.SPI, addr uint8, value uint16, order bi return bus.Tx(d.buf[:3], nil) } +// Write32SPI writes a 32-bit value to register addr using the provided SPI bus. The byte order is specified by order. func (d *Device8) Write32SPI(bus drivers.SPI, addr uint8, value uint32, order binary.ByteOrder) error { d.clear() d.buf[0] = addr diff --git a/internal/regmap/i2c-spi.go b/internal/regmap/i2c-spi.go new file mode 100644 index 0000000..0e0abe7 --- /dev/null +++ b/internal/regmap/i2c-spi.go @@ -0,0 +1,123 @@ +package regmap + +import ( + "encoding/binary" + + "tinygo.org/x/drivers" +) + +// Device8SPI implements common logic to most 8-bit peripherals with an SPI bus. +// All methods expect the target to support conventional register read and write operations +// where the first byte sent is the register address being accessed. +// +// All methods use an internal buffer and perform no dynamic memory allocation. +type Device8SPI struct { + bus drivers.SPI + order binary.ByteOrder + d Device8 +} + +// SetBus sets the SPI bus and byte order for the Device8SPI. +// +// As a hint, most SPI devices use big-endian (MSB) byte order. +// - Big endian: A value of 0x1234 is transmitted as 0x12 followed by 0x34. +// - Little endian: A value of 0x1234 is transmitted as 0x34 followed by 0x12. +func (d *Device8SPI) SetBus(bus drivers.SPI, order binary.ByteOrder) { + d.bus = bus + d.order = order +} + +// Read8 reads a single byte from register addr. +func (d *Device8SPI) Read8(addr uint8) (byte, error) { + return d.d.Read8SPI(d.bus, addr) +} + +// Read16 reads a 16-bit value from register addr. +func (d *Device8SPI) Read16(addr uint8) (uint16, error) { + return d.d.Read16SPI(d.bus, addr, d.order) +} + +// Read32 reads a 32-bit value from register addr. +func (d *Device8SPI) Read32(addr uint8) (uint32, error) { + return d.d.Read32SPI(d.bus, addr, d.order) +} + +// ReadData reads dataLength bytes from register addr. Due to the internal functioning of +// SPI, an auxiliary buffer must be provided to perform the operation and avoid memory allocation. +// The returned slice is a subslice of auxBuffer containing the read data. +func (d *Device8SPI) ReadData(addr uint8, datalength int, auxBuffer []byte) ([]byte, error) { + return d.d.ReadDataSPI(d.bus, addr, datalength, auxBuffer) +} + +// Write8 writes a single byte value to register addr. +func (d *Device8SPI) Write8(addr, value uint8) error { + return d.d.Write8SPI(d.bus, addr, value) +} + +// Write16 writes a 16-bit value to register addr. +func (d *Device8SPI) Write16(addr uint8, value uint16) error { + return d.d.Write16SPI(d.bus, addr, value, d.order) +} + +// Write32 writes a 32-bit value to register addr. +func (d *Device8SPI) Write32(addr uint8, value uint32) error { + return d.d.Write32SPI(d.bus, addr, value, d.order) +} + +// Device8I2C implements common logic to most 8-bit peripherals with an I2C bus. +// All methods expect the target to support conventional register read and write operations +// where the first byte sent is the register address being accessed. +// +// All methods use an internal buffer and perform no dynamic memory allocation. +type Device8I2C struct { + bus drivers.I2C + i2cAddr uint16 + order binary.ByteOrder + d Device8 +} + +// SetBus sets the I2C bus, device address, and byte order for the Device8I2C. +// +// As a hint, most I2C devices use big-endian (MSB) byte order. +// - Big endian: A value of 0x1234 is transmitted as 0x12 followed by 0x34. +// - Little endian: A value of 0x1234 is transmitted as 0x34 followed by 0x12. +func (d *Device8I2C) SetBus(bus drivers.I2C, i2cAddr uint16, order binary.ByteOrder) { + d.bus = bus + d.i2cAddr = i2cAddr + d.order = order +} + +// Read8 reads a single byte from register addr. +func (d *Device8I2C) Read8(addr uint8) (byte, error) { + return d.d.Read8I2C(d.bus, d.i2cAddr, addr) +} + +// Read16 reads a 16-bit value from register addr. +func (d *Device8I2C) Read16(addr uint8) (uint16, error) { + return d.d.Read16I2C(d.bus, d.i2cAddr, addr, d.order) +} + +// Read32 reads a 32-bit value from register addr. +func (d *Device8I2C) Read32(addr uint8) (uint32, error) { + return d.d.Read32I2C(d.bus, d.i2cAddr, addr, d.order) +} + +// ReadData reads dataLength bytes from register addr. +func (d *Device8I2C) ReadData(addr uint8, dataDestination []byte) error { + return d.d.ReadDataI2C(d.bus, d.i2cAddr, addr, dataDestination) +} + +// Write8 writes a single byte value to register addr. +func (d *Device8I2C) Write8(addr, value uint8) error { + return d.d.Write8I2C(d.bus, d.i2cAddr, addr, value) +} + +// Write16 writes a 16-bit value to register addr. +func (d *Device8I2C) Write16(addr uint8, value uint16) error { + return d.d.Write16I2C(d.bus, d.i2cAddr, addr, value, d.order) +} + +// Write32 writes a 32-bit value to register addr. +func (d *Device8I2C) Write32(addr uint8, value uint32) error { + return d.d.Write32I2C(d.bus, d.i2cAddr, addr, value, d.order) +} From 408851a9f58d60ce55177b55d67bfc9244319e39 Mon Sep 17 00:00:00 2001 From: Ron Evans Date: Mon, 10 Nov 2025 12:04:25 +0100 Subject: [PATCH 092/137] si5351: add support for si5351 (#810) * si5351: add support for si5351 Adds support for the si5351 I2C programmable clock generator using code from @chiefMarlin which used code from @conotto which somehow never got merged. Thank you everyone! Signed-off-by: deadprogram * refactor: use regmap instead of legacy package to avoid heap allocations Signed-off-by: deadprogram --------- Signed-off-by: deadprogram --- examples/si5351/main.go | 107 ++++++++++ si5351/registers.go | 64 ++++++ si5351/si5351.go | 448 ++++++++++++++++++++++++++++++++++++++++ smoketest.sh | 1 + 4 files changed, 620 insertions(+) create mode 100644 examples/si5351/main.go create mode 100644 si5351/registers.go create mode 100644 si5351/si5351.go diff --git a/examples/si5351/main.go b/examples/si5351/main.go new file mode 100644 index 0000000..bd49ab0 --- /dev/null +++ b/examples/si5351/main.go @@ -0,0 +1,107 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/si5351" +) + +// Simple demo of the SI5351 clock generator. +// This is like the Arduino library example: +// https://github.com/adafruit/Adafruit_Si5351_Library/blob/master/examples/si5351/si5351.ino +// Which will configure the chip with: +// - PLL A at 900mhz +// - PLL B at 616.66667mhz +// - Clock 0 at 112.5mhz, using PLL A as a source divided by 8 +// - Clock 1 at 13.5531mhz, using PLL B as a source divided by 45.5 +// - Clock 2 at 10.76khz, using PLL B as a source divided by 900 and further divided with an R divider of 64. + +func main() { + time.Sleep(5 * time.Second) + + println("Si5351 Clockgen Test") + println() + + // Configure I2C bus + machine.I2C0.Configure(machine.I2CConfig{}) + + // Create driver instance + clockgen := si5351.New(machine.I2C0) + + // Verify device wired properly + connected, err := clockgen.Connected() + if err != nil { + println("Unable to read device status") + time.Sleep(time.Second) + } + if !connected { + for { + println("Unable to detect si5351 device") + time.Sleep(time.Second) + } + } + + // Initialise device + clockgen.Configure() + + // Now configue the PLLs and clock outputs. + // The PLLs can be configured with a multiplier and division of the on-board + // 25mhz reference crystal. For example configure PLL A to 900mhz by multiplying + // by 36. This uses an integer multiplier which is more accurate over time + // but allows less of a range of frequencies compared to a fractional + // multiplier shown next. + clockgen.ConfigurePLL(si5351.PLL_A, 36, 0, 1) // Multiply 25mhz by 36 + println("PLL A frequency: 900mhz") + + // And next configure PLL B to 616.6667mhz by multiplying 25mhz by 24.667 using + // the fractional multiplier configuration. Notice you specify the integer + // multiplier and then a numerator and denominator as separate values, i.e. + // numerator 2 and denominator 3 means 2/3 or 0.667. This fractional + // configuration is susceptible to some jitter over time but can set a larger + // range of frequencies. + clockgen.ConfigurePLL(si5351.PLL_B, 24, 2, 3) // Multiply 25mhz by 24.667 (24 2/3) + println("PLL B frequency: 616.6667mhz") + + // Now configure the clock outputs. Each is driven by a PLL frequency as input + // and then further divides that down to a specific frequency. + // Configure clock 0 output to be driven by PLL A divided by 8, so an output + // of 112.5mhz (900mhz / 8). Again this uses the most precise integer division + // but can't set as wide a range of values. + clockgen.ConfigureMultisynth(0, si5351.PLL_A, 8, 0, 1) // Divide by 8 (8 0/1) + println("Clock 0: 112.5mhz") + + // Next configure clock 1 to be driven by PLL B divided by 45.5 to get + // 13.5531mhz (616.6667mhz / 45.5). This uses fractional division and again + // notice the numerator and denominator are explicitly specified. This is less + // precise but allows a large range of frequencies. + clockgen.ConfigureMultisynth(1, si5351.PLL_B, 45, 1, 2) // Divide by 45.5 (45 1/2) + println("Clock 1: 13.5531mhz") + + // Finally configure clock 2 to be driven by PLL B divided once by 900 to get + // down to 685.15 khz and then further divided by a special R divider that + // divides 685.15 khz by 64 to get a final output of 10.706khz. + clockgen.ConfigureMultisynth(2, si5351.PLL_B, 900, 0, 1) // Divide by 900 (900 0/1) + // Set the R divider, this can be a value of: + // - R_DIV_1: divider of 1 + // - R_DIV_2: divider of 2 + // - R_DIV_4: divider of 4 + // - R_DIV_8: divider of 8 + // - R_DIV_16: divider of 16 + // - R_DIV_32: divider of 32 + // - R_DIV_64: divider of 64 + // - R_DIV_128: divider of 128 + clockgen.ConfigureRdiv(2, si5351.R_DIV_64) + println("Clock 2: 10.706khz") + + // After configuring PLLs and clocks, enable the outputs. + clockgen.EnableOutputs() + + for { + time.Sleep(5 * time.Second) + println() + println("Clock 0: 112.5mhz") + println("Clock 1: 13.5531mhz") + println("Clock 2: 10.706khz") + } +} diff --git a/si5351/registers.go b/si5351/registers.go new file mode 100644 index 0000000..79300ad --- /dev/null +++ b/si5351/registers.go @@ -0,0 +1,64 @@ +package si5351 + +// The I2C address which this device listens to. +const AddressDefault = 0x60 // Assumes ADDR pin is low +const AddressAlternative = 0x61 // Assumes ADDR pin is high + +const ( + OUTPUT_ENABLE_CONTROL = 3 + + CLK0_CONTROL = 16 + CLK1_CONTROL = 17 + CLK2_CONTROL = 18 + CLK3_CONTROL = 19 + CLK4_CONTROL = 20 + CLK5_CONTROL = 21 + CLK6_CONTROL = 22 + CLK7_CONTROL = 23 + + MULTISYNTH0_PARAMETERS_1 = 42 + MULTISYNTH0_PARAMETERS_3 = 44 + MULTISYNTH1_PARAMETERS_1 = 50 + MULTISYNTH1_PARAMETERS_3 = 52 + MULTISYNTH2_PARAMETERS_1 = 58 + MULTISYNTH2_PARAMETERS_3 = 60 + + SPREAD_SPECTRUM_PARAMETERS = 149 + + PLL_RESET = 177 + + CRYSTAL_INTERNAL_LOAD_CAPACITANCE = 183 +) + +const ( + CRYSTAL_LOAD_6PF = (1 << 6) + CRYSTAL_LOAD_8PF = (2 << 6) + CRYSTAL_LOAD_10PF = (3 << 6) +) + +const ( + CRYSTAL_FREQ_25MHZ = 25000000 + CRYSTAL_FREQ_27MHZ = 27000000 +) + +const ( + PLL_A = iota + PLL_B +) + +const ( + R_DIV_1 = iota + R_DIV_2 + R_DIV_4 + R_DIV_8 + R_DIV_16 + R_DIV_32 + R_DIV_64 + R_DIV_128 +) + +const ( + MULTISYNTH_DIV_4 = 4 + MULTISYNTH_DIV_6 = 6 + MULTISYNTH_DIV_8 = 8 +) diff --git a/si5351/si5351.go b/si5351/si5351.go new file mode 100644 index 0000000..e7cc623 --- /dev/null +++ b/si5351/si5351.go @@ -0,0 +1,448 @@ +package si5351 + +import ( + "encoding/binary" + "errors" + "fmt" + "math" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/regmap" +) + +// Device wraps an I2C connection to a SI5351 device. +type Device struct { + bus drivers.I2C + Address uint8 + + rw regmap.Device8I2C + initialised bool + crystalFreq uint32 + crystalLoad uint8 + pllaConfigured bool + pllaFreq uint32 + pllbConfigured bool + pllbFreq uint32 + lastRdivValue [3]uint8 +} + +var ErrNotInitialised = errors.New("Si5351 not initialised") +var ErrInvalidParameter = errors.New("Si5351 invalid parameter") + +// New creates a new SI5351 connection. The I2C bus must already be configured. +// +// This function only creates the Device object, it does not touch the device. +func New(bus drivers.I2C) Device { + rw := regmap.Device8I2C{} + rw.SetBus(bus, AddressDefault, binary.BigEndian) + + return Device{ + bus: bus, + rw: rw, + Address: AddressDefault, + crystalFreq: CRYSTAL_FREQ_25MHZ, + crystalLoad: CRYSTAL_LOAD_10PF, + } +} + +// Configure sets up the device for communication +// TODO error handling +func (d *Device) Configure() error { + // // Disable all outputs setting CLKx_DIS high + d.rw.Write8(OUTPUT_ENABLE_CONTROL, 0xFF) + + // Set the load capacitance for the XTAL + d.rw.Write8(CRYSTAL_INTERNAL_LOAD_CAPACITANCE, d.crystalLoad) + + // Power down all output drivers + buf := []byte{CLK0_CONTROL, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80} + d.bus.Tx(uint16(d.Address), buf, nil) + + // Disable spread spectrum output. + if err := d.DisableSpreadSpectrum(); err != nil { + return err + } + + d.initialised = true + + return nil +} + +// Connected returns whether a device at SI5351 address has been found. +func (d *Device) Connected() (bool, error) { + if err := d.bus.Tx(uint16(d.Address), []byte{}, []byte{0}); err != nil { + return false, err + } + return true, nil +} + +// EnableSpreadSpectrum enables spread spectrum modulation to reduce EMI. +func (d *Device) EnableSpreadSpectrum() error { + data, err := d.rw.Read8(SPREAD_SPECTRUM_PARAMETERS) + if err != nil { + return err + } + + data |= 0x80 + return d.rw.Write8(SPREAD_SPECTRUM_PARAMETERS, data) +} + +func (d *Device) DisableSpreadSpectrum() error { + data, err := d.rw.Read8(SPREAD_SPECTRUM_PARAMETERS) + if err != nil { + return err + } + + data &^= 0x80 + return d.rw.Write8(SPREAD_SPECTRUM_PARAMETERS, data) +} + +func (d *Device) OutputEnable(output uint8, enable bool) error { + if !d.initialised { + return ErrNotInitialised + } + + // Read the current value of the OUTPUT_ENABLE_CONTROL register + regVal, err := d.rw.Read8(OUTPUT_ENABLE_CONTROL) + if err != nil { + return err + } + + // Modify regVal based on clk and enable + if enable { + regVal &= ^(1 << output) + } else { + regVal |= (1 << output) + } + + // Write the modified value back to the OUTPUT_ENABLE_CONTROL register + return d.rw.Write8(OUTPUT_ENABLE_CONTROL, regVal) +} + +func (d *Device) EnableOutputs() error { + if !d.initialised { + return ErrNotInitialised + } + + return d.rw.Write8(OUTPUT_ENABLE_CONTROL, 0x00) +} + +func (d *Device) DisableOutputs() error { + if !d.initialised { + return ErrNotInitialised + } + return d.rw.Write8(OUTPUT_ENABLE_CONTROL, 0xFF) +} + +// ConfigurePLL sets the multiplier for the specified PLL +// pll The PLL to configure, which must be one of the following: +// - PLL_A +// - PLL_B +// +// mult The PLL integer multiplier (must be between 15 and 90) +// +// num The 20-bit numerator for fractional output (0..1,048,575). +// Set this to '0' for integer output. +// +// denom The 20-bit denominator for fractional output (1..1,048,575). +// Set this to '1' or higher to avoid divider by zero errors. +// +// PLL Configuration +// fVCO is the PLL output, and must be between 600..900MHz, where: +// +// fVCO = fXTAL * (a+(b/c)) +// +// fXTAL = the crystal input frequency +// a = an integer between 15 and 90 +// b = the fractional numerator (0..1,048,575) +// c = the fractional denominator (1..1,048,575) +// +// NOTE: Try to use integers whenever possible to avoid clock jitter +// (only use the a part, setting b to '0' and c to '1'). +// +// See: http://www.silabs.com/Support%20Documents/TechnicalDocs/AN619.pdf +func (d *Device) ConfigurePLL(pll uint8, mult uint8, num uint32, denom uint32) error { + // Basic validation + if !d.initialised { + return ErrNotInitialised + } + // mult = 15..90 + if !((mult > 14) && (mult < 91)) { + return ErrInvalidParameter + } + // Avoid divide by zero + if !(denom > 0) { + return ErrInvalidParameter + } + // 20-bit limit + if !(num <= 0xFFFFF) { + return ErrInvalidParameter + } + // 20-bit limit + if !(denom <= 0xFFFFF) { + return ErrInvalidParameter + } + + // PLL Multiplier Equations + // + // P1 register is an 18-bit value using following formula: + // + // P1[17:0] = 128 * mult + floor(128*(num/denom)) - 512 + // + // P2 register is a 20-bit value using the following formula: + // + // P2[19:0] = 128 * num - denom * floor(128*(num/denom)) + // + // P3 register is a 20-bit value using the following formula: + // + // P3[19:0] = denom + // + + // Set PLL config registers + var p1, p2, p3 uint32 + if num == 0 { + // Integer mode + p1 = 128*uint32(mult) - 512 + p2 = num + p3 = denom + } else { + // Fractional mode + p1 = uint32(128*float64(mult) + math.Floor(128*(float64(num)/float64(denom))) - 512) + p2 = uint32(128*float64(num) - float64(denom)*math.Floor(128*(float64(num)/float64(denom)))) + p3 = denom + } + + // Get the appropriate starting point for the PLL registers + baseaddr := uint8(26) + if pll == PLL_B { + baseaddr = 34 + } + + // The datasheet is a nightmare of typos and inconsistencies here! + data := [8]byte{} + data[0] = uint8((p3 & 0x0000FF00) >> 8) + data[1] = uint8(p3 & 0x000000FF) + data[2] = uint8((p1 & 0x00030000) >> 16) + data[3] = uint8((p1 & 0x0000FF00) >> 8) + data[4] = uint8(p1 & 0x000000FF) + data[5] = uint8(((p3 & 0x000F0000) >> 12) | ((p2 & 0x000F0000) >> 16)) + data[6] = uint8((p2 & 0x0000FF00) >> 8) + data[7] = uint8(p2 & 0x000000FF) + if err := d.bus.Tx(uint16(baseaddr), data[:], nil); err != nil { + return err + } + + // Reset both PLLs + if err := d.rw.Write8(PLL_RESET, (1<<7)|(1<<5)); err != nil { + return err + } + + // Store the frequency settings for use with the Multisynth helper + fvco := float64(d.crystalFreq) * (float64(mult) + (float64(num) / float64(denom))) + if pll == PLL_A { + d.pllaConfigured = true + d.pllaFreq = uint32(math.Floor(fvco)) + } else { + d.pllbConfigured = true + d.pllbFreq = uint32(math.Floor(fvco)) + } + return nil +} + +// ConfigureMultisynth divider, which determines the +// output clock frequency based on the specified PLL input. +// +// output The output channel to use (0..2) +// +// pll The PLL input source to use, which must be one of: +// - PLL_A +// - PLL_B +// +// div The integer divider for the Multisynth output. +// +// If pure integer values are used, this value must be one of: +// - MULTISYNTH_DIV_4 +// - MULTISYNTH_DIV_6 +// - MULTISYNTH_DIV_8 +// If fractional output is used, this value must be between 8 and 900. +// +// num The 20-bit numerator for fractional output (0..1,048,575). +// +// Set this to '0' for integer output. +// +// denom The 20-bit denominator for fractional output (1..1,048,575). +// +// Set this to '1' or higher to avoid divide by zero errors. +// +// # Output Clock Configuration +// +// The multisynth dividers are applied to the specified PLL output, +// and are used to reduce the PLL output to a valid range (500kHz +// to 160MHz). The relationship can be seen in this formula, where +// fVCO is the PLL output frequency and MSx is the multisynth divider: +// +// fOUT = fVCO / MSx +// +// Valid multisynth dividers are 4, 6, or 8 when using integers, +// or any fractional values between 8 + 1/1,048,575 and 900 + 0/1 +// The following formula is used for the fractional mode divider: +// +// a + b / c +// +// a = The integer value, which must be 4, 6 or 8 in integer mode (MSx_INT=1) or 8..900 in fractional mode (MSx_INT=0). +// b = The fractional numerator (0..1,048,575) +// c = The fractional denominator (1..1,048,575) +// +// NOTE: Try to use integers whenever possible to avoid clock jitter +// NOTE: For output frequencies > 150MHz, you must set the divider +// +// to 4 and adjust to PLL to generate the frequency (for example +// a PLL of 640 to generate a 160MHz output clock). This is not +// yet supported in the driver, which limits frequencies to 500kHz .. 150MHz. +// +// NOTE: For frequencies below 500kHz (down to 8kHz) Rx_DIV must be +// +// used, but this isn't currently implemented in the driver. +func (d *Device) ConfigureMultisynth(output uint8, pll uint8, div uint32, num uint32, denom uint32) error { + // Basic validation + if !d.initialised { + return ErrNotInitialised + } + // Channel range + if !(output < 3) { + return fmt.Errorf("output channel must be between 0 and 2") + } + // Divider integer value + if !((div > 3) && (div < 2049)) { + return ErrInvalidParameter + } + // Avoid divide by zero + if !(denom > 0) { + return ErrInvalidParameter + } + // 20-bit limit + if !(num <= 0xFFFFF) { + return ErrInvalidParameter + } + // 20-bit limit + if !(denom <= 0xFFFFF) { + return ErrInvalidParameter + } + + // Make sure the requested PLL has been initialised + if pll == PLL_A && !d.pllaConfigured { + return ErrInvalidParameter + } + if pll == PLL_B && !d.pllbConfigured { + return ErrInvalidParameter + } + + // Output Multisynth Divider Equations + // + // where: a = div, b = num and c = denom + // + // P1 register is an 18-bit value using following formula: + // + // P1[17:0] = 128 * a + floor(128*(b/c)) - 512 + // + // P2 register is a 20-bit value using the following formula: + // + // P2[19:0] = 128 * b - c * floor(128*(b/c)) + // + // P3 register is a 20-bit value using the following formula: + // + // P3[19:0] = c + // + + // Set PLL config registers + var p1, p2, p3 uint32 + if num == 0 { + // Integer mode + p1 = 128*div - 512 + p2 = 0 + p3 = denom + } else if denom == 1 { + // Fractional mode, simplified calculations + p1 = 128*div + 128*num - 512 + p2 = 128*num - 128 + p3 = 1 + } else { + // Fractional mode + p1 = uint32(128*float64(div) + math.Floor(128*(float64(num)/float64(denom))) - 512) + p2 = uint32(128*float64(num) - float64(denom)*math.Floor(128*(float64(num)/float64(denom)))) + p3 = denom + } + + // Get the appropriate starting point for the PLL registers + baseaddr := uint8(0) + switch output { + case 0: + baseaddr = MULTISYNTH0_PARAMETERS_1 + case 1: + baseaddr = MULTISYNTH1_PARAMETERS_1 + case 2: + baseaddr = MULTISYNTH2_PARAMETERS_1 + } + + // Set the MSx config registers + data := [8]byte{} + data[0] = uint8((p3 & 0xFF00) >> 8) + data[1] = uint8(p3 & 0xFF) + data[2] = uint8(((p1 & 0x30000) >> 16)) | d.lastRdivValue[output] + data[3] = uint8((p1 & 0xFF00) >> 8) + data[4] = uint8(p1 & 0xFF) + data[5] = uint8(((p3 & 0xF0000) >> 12) | ((p2 & 0xF0000) >> 16)) + data[6] = uint8((p2 & 0xFF00) >> 8) + data[7] = uint8(p2 & 0xFF) + if err := d.bus.Tx(uint16(baseaddr), data[:], nil); err != nil { + return err + } + + // Configure the clk control and enable the output + // TODO: Check if the clk control byte needs to be updated. + clkControlReg := uint8(0x0F) // 8mA drive strength, MS0 as CLK0 source, Clock not inverted, powered up + if pll == PLL_B { + clkControlReg |= (1 << 5) // Uses PLLB + } + if num == 0 { + clkControlReg |= (1 << 6) // Integer mode + } + + var register uint8 + switch output { + case 0: + register = CLK0_CONTROL + case 1: + register = CLK1_CONTROL + case 2: + register = CLK2_CONTROL + } + + return d.rw.Write8(register, clkControlReg) +} + +func (d *Device) ConfigureRdiv(output uint8, div uint8) error { + // Channel range + if !(output < 3) { + return ErrInvalidParameter + } + + var register uint8 + switch output { + case 0: + register = MULTISYNTH0_PARAMETERS_3 + case 1: + register = MULTISYNTH1_PARAMETERS_3 + case 2: + register = MULTISYNTH2_PARAMETERS_3 + } + + data, err := d.rw.Read8(register) + if err != nil { + return err + } + + d.lastRdivValue[output] = (div & 0x07) << 4 + data = (data & 0x0F) | d.lastRdivValue[output] + return d.rw.Write8(register, data) +} diff --git a/smoketest.sh b/smoketest.sh index b003598..ff10692 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/si5351/main.go # network examples (espat) tinygo build -size short -o ./build/test.hex -target=challenger-rp2040 ./examples/net/ntpclient/ # network examples (wifinina) From e35e6b8e13bf7ae1147989e652d9410e913f88d2 Mon Sep 17 00:00:00 2001 From: sago35 Date: Mon, 10 Nov 2025 19:31:38 +0900 Subject: [PATCH 093/137] fix: correct logic error in image size checks in pixel's tests --- pixel/image_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pixel/image_test.go b/pixel/image_test.go index 2b804b6..1feef75 100644 --- a/pixel/image_test.go +++ b/pixel/image_test.go @@ -11,7 +11,7 @@ import ( func TestImageRGB565BE(t *testing.T) { image := pixel.NewImage[pixel.RGB565BE](5, 3) - if width, height := image.Size(); width != 5 && height != 3 { + if width, height := image.Size(); width != 5 || height != 3 { t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height) } for _, c := range []color.RGBA{ @@ -32,7 +32,7 @@ func TestImageRGB565BE(t *testing.T) { func TestImageRGB444BE(t *testing.T) { image := pixel.NewImage[pixel.RGB444BE](5, 3) - if width, height := image.Size(); width != 5 && height != 3 { + if width, height := image.Size(); width != 5 || height != 3 { t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height) } for _, c := range []color.RGBA{ From 5847506ba6123ada1140f83d8f5cdd8414567ac5 Mon Sep 17 00:00:00 2001 From: sago35 Date: Mon, 10 Nov 2025 19:33:18 +0900 Subject: [PATCH 094/137] Add TestImageRGB888 and TestImageRGB555 --- pixel/image_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/pixel/image_test.go b/pixel/image_test.go index 1feef75..b0963b1 100644 --- a/pixel/image_test.go +++ b/pixel/image_test.go @@ -9,6 +9,27 @@ import ( "tinygo.org/x/drivers/pixel" ) +func TestImageRGB888(t *testing.T) { + image := pixel.NewImage[pixel.RGB888](5, 3) + if width, height := image.Size(); width != 5 || height != 3 { + t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height) + } + for _, c := range []color.RGBA{ + {R: 0xff, A: 0xff}, + {G: 0xff, A: 0xff}, + {B: 0xff, A: 0xff}, + {R: 0x10, A: 0xff}, + {G: 0x10, A: 0xff}, + {B: 0x10, A: 0xff}, + } { + image.Set(4, 2, pixel.NewColor[pixel.RGB888](c.R, c.G, c.B)) + c2 := image.Get(4, 2).RGBA() + if c2 != c { + t.Errorf("failed to roundtrip color: expected %v but got %v", c, c2) + } + } +} + func TestImageRGB565BE(t *testing.T) { image := pixel.NewImage[pixel.RGB565BE](5, 3) if width, height := image.Size(); width != 5 || height != 3 { @@ -30,6 +51,27 @@ func TestImageRGB565BE(t *testing.T) { } } +func TestImageRGB555(t *testing.T) { + image := pixel.NewImage[pixel.RGB555](5, 3) + if width, height := image.Size(); width != 5 || height != 3 { + t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height) + } + for _, c := range []color.RGBA{ + {R: 0xff, A: 0xff}, + {G: 0xff, A: 0xff}, + {B: 0xff, A: 0xff}, + {R: 0x10, A: 0xff}, + {G: 0x10, A: 0xff}, + {B: 0x10, A: 0xff}, + } { + image.Set(4, 2, pixel.NewColor[pixel.RGB555](c.R, c.G, c.B)) + c2 := image.Get(4, 2).RGBA() + if c2 != c { + t.Errorf("failed to roundtrip color: expected %v but got %v", c, c2) + } + } +} + func TestImageRGB444BE(t *testing.T) { image := pixel.NewImage[pixel.RGB444BE](5, 3) if width, height := image.Size(); width != 5 || height != 3 { From 027c91272e08c3f898371f345c200cadb4d19d2c Mon Sep 17 00:00:00 2001 From: sago35 Date: Mon, 10 Nov 2025 19:33:39 +0900 Subject: [PATCH 095/137] pixel: correct RGB555 to RGBA conversion logic --- pixel/pixel.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pixel/pixel.go b/pixel/pixel.go index ed91a60..b4582a6 100644 --- a/pixel/pixel.go +++ b/pixel/pixel.go @@ -161,9 +161,9 @@ func (c RGB555) BitsPerPixel() int { func (c RGB555) RGBA() color.RGBA { color := color.RGBA{ - R: uint8(c>>10) << 3, - G: uint8(c>>5) << 3, - B: uint8(c) << 3, + R: (uint8(c) & 0x1F) << 3, + G: (uint8(c>>5) & 0x1F) << 3, + B: (uint8(c>>10) & 0x1F) << 3, A: 255, } // Correct color rounding, so that 0xff roundtrips back to 0xff. From 744fda5eecd83cc48f73cac34b696697c8ac14d4 Mon Sep 17 00:00:00 2001 From: sago35 Date: Mon, 10 Nov 2025 19:47:15 +0900 Subject: [PATCH 096/137] fix: correct logic error in image size checks in pixel's tests (Monochrome) --- pixel/image_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pixel/image_test.go b/pixel/image_test.go index b0963b1..5636f3d 100644 --- a/pixel/image_test.go +++ b/pixel/image_test.go @@ -109,7 +109,7 @@ func TestImageRGB444BE(t *testing.T) { func TestImageMonochrome(t *testing.T) { image := pixel.NewImage[pixel.Monochrome](128, 64) - if width, height := image.Size(); width != 128 && height != 64 { + if width, height := image.Size(); width != 128 || height != 64 { t.Errorf("image.Size(): expected 128, 64 but got %d, %d", width, height) } for _, expected := range []color.RGBA{ From 23833e69c79625454b326f898ada3dc81d40f06c Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:22:28 +0100 Subject: [PATCH 097/137] DS3231 Alarm features (#805) * Added alarm features * more functions * chore: fix typo * feat(ds3231): add Alarm2Mode type and mode consts * docs(ds3231): add docstrings for SQW functions * feat(ds3231): add methods for Alarm2 * fix(ds3231): use Alarm2Mode for SetAlarm2 method * docs(ds3231): add example for alarms * docs(ds3231): refactor alarms example * make main function more concise to avoid llvm error for pico * ci(ds3231): split basic and alarm tests * style(ds3231): reorder private funcs to the bottom * docs(ds3231): add docstring for alarm modes * docs(ds3231): make alarm docstrings more descriptive * chore(ds3231): fix typo in docstring * style(ds3231): reorder public functions * style(ds3231): reorder private methods * chore(ds3231): add missing error handling * feat(ds3231): use setter funcs for en/disabling instead of separate funcs * fix(ds3231): correctly enable alarms in example * style(ds3231): rename SetEnable32K to SetEnabled32K for consistency * refactor(ds3231): replace legacy with regmap package * refactor(ds3231): use Write32 instead of Tx for SetAlarm1 * chore(ds3231): remove fmt deps and and use println in examples * refactor(ds3231): read temperature as uint16 --------- Co-authored-by: Matthias Fulz --- ds3231/ds3231.go | 324 ++++++++++++++++++++++++---- ds3231/ds3231_test.go | 26 +-- ds3231/registers.go | 49 +++++ examples/ds3231/alarms/main.go | 74 +++++++ examples/ds3231/{ => basic}/main.go | 11 +- smoketest.sh | 3 +- 6 files changed, 431 insertions(+), 56 deletions(-) create mode 100644 examples/ds3231/alarms/main.go rename examples/ds3231/{ => basic}/main.go (68%) diff --git a/ds3231/ds3231.go b/ds3231/ds3231.go index 4c6f119..9679a9d 100644 --- a/ds3231/ds3231.go +++ b/ds3231/ds3231.go @@ -5,10 +5,12 @@ package ds3231 // import "tinygo.org/x/drivers/ds3231" import ( + "encoding/binary" + "errors" "time" "tinygo.org/x/drivers" - "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/internal/regmap" ) type Mode uint8 @@ -17,6 +19,7 @@ type Mode uint8 type Device struct { bus drivers.I2C Address uint16 + d regmap.Device8I2C } // New creates a new DS3231 connection. The I2C bus must already be @@ -24,54 +27,50 @@ type Device struct { // // This function only creates the Device object, it does not touch the device. func New(bus drivers.I2C) Device { - return Device{ + d := Device{ bus: bus, Address: Address, } + d.Configure() + return d } // Configure sets up the device for communication func (d *Device) Configure() bool { + d.d.SetBus(d.bus, d.Address, binary.BigEndian) return true } // IsTimeValid return true/false is the time in the device is valid func (d *Device) IsTimeValid() bool { - data := []byte{0} - err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_STATUS, data) + status, err := d.d.Read8(REG_STATUS) if err != nil { return false } - return (data[0] & (1 << OSF)) == 0x00 + return (status & (1 << OSF)) == 0x00 } // IsRunning returns if the oscillator is running func (d *Device) IsRunning() bool { - data := []uint8{0} - err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CONTROL, data) + control, err := d.d.Read8(REG_CONTROL) if err != nil { return false } - return (data[0] & (1 << EOSC)) == 0x00 + return (control & (1 << EOSC)) == 0x00 } // SetRunning starts the internal oscillator func (d *Device) SetRunning(isRunning bool) error { - data := []uint8{0} - err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CONTROL, data) + control, err := d.d.Read8(REG_CONTROL) if err != nil { return err } if isRunning { - data[0] &^= uint8(1 << EOSC) + control &^= uint8(1 << EOSC) } else { - data[0] |= 1 << EOSC + control |= 1 << EOSC } - err = legacy.WriteRegister(d.bus, uint8(d.Address), REG_CONTROL, data) - if err != nil { - return err - } - return nil + return d.d.Write8(REG_CONTROL, control) } // SetTime sets the date and time in the DS3231. The DS3231 hardware supports @@ -86,18 +85,16 @@ func (d *Device) SetRunning(isRunning bool) error { // 2100 as a leap year, causing it to increment from 2100-02-28 to 2100-02-29 // instead of 2100-03-01. func (d *Device) SetTime(dt time.Time) error { - data := []byte{0} - err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_STATUS, data) + status, err := d.d.Read8(REG_STATUS) if err != nil { return err } - data[0] &^= 1 << OSF - err = legacy.WriteRegister(d.bus, uint8(d.Address), REG_STATUS, data) - if err != nil { + status &^= 1 << OSF + if err = d.d.Write8(REG_STATUS, status); err != nil { return err } - data = make([]uint8, 7) + data := make([]uint8, 7) data[0] = uint8ToBCD(uint8(dt.Second())) data[1] = uint8ToBCD(uint8(dt.Minute())) data[2] = uint8ToBCD(uint8(dt.Hour())) @@ -118,21 +115,16 @@ func (d *Device) SetTime(dt time.Time) error { data[5] = uint8ToBCD(uint8(dt.Month()) | centuryFlag) data[6] = uint8ToBCD(year) - err = legacy.WriteRegister(d.bus, uint8(d.Address), REG_TIMEDATE, data) - if err != nil { - return err - } - - return nil + return d.bus.Tx(d.Address, append([]byte{REG_TIMEDATE}, data...), nil) } // ReadTime returns the date and time func (d *Device) ReadTime() (dt time.Time, err error) { data := make([]uint8, 7) - err = legacy.ReadRegister(d.bus, uint8(d.Address), REG_TIMEDATE, data) - if err != nil { + if err = d.d.ReadData(REG_TIMEDATE, data); err != nil { return } + second := bcdToInt(data[0] & 0x7F) minute := bcdToInt(data[1]) hour := hoursBCDToInt(data[2]) @@ -150,12 +142,264 @@ func (d *Device) ReadTime() (dt time.Time, err error) { // ReadTemperature returns the temperature in millicelsius (mC) func (d *Device) ReadTemperature() (int32, error) { - data := make([]uint8, 2) - err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_TEMP, data) + temp, err := d.d.Read16(REG_TEMP) if err != nil { return 0, err } - return milliCelsius(data[0], data[1]), nil + return milliCelsius(temp), nil +} + +// GetSqwPinMode returns the current square wave output frequency +func (d *Device) GetSqwPinMode() SqwPinMode { + control, err := d.d.Read8(REG_CONTROL) + if err != nil { + return SQW_OFF + } + + control &= 0x1C // turn off INTCON + if control&0x04 != 0 { + return SQW_OFF + } + + return SqwPinMode(control) +} + +// SetSqwPinMode sets the square wave output mode to the given frequency +func (d *Device) SetSqwPinMode(mode SqwPinMode) error { + control, err := d.d.Read8(REG_CONTROL) + if err != nil { + return err + } + + control &^= 0x04 // turn off INTCON + control &^= 0x18 // set freq bits to 0 + + control |= uint8(mode) + + return d.d.Write8(REG_CONTROL, control) +} + +// SetAlarm1 sets alarm1 to the given time and mode +func (d *Device) SetAlarm1(dt time.Time, mode Alarm1Mode) error { + control, err := d.d.Read8(REG_CONTROL) + if err != nil { + return err + } + if control&(1< 0 { + day = dowToDS3231(int(dt.Weekday())) + } + + alarm1 := uint32(uint8ToBCD(uint8(dt.Second()))|A1M1) << 24 + alarm1 |= uint32(uint8ToBCD(uint8(dt.Minute()))|A1M2) << 16 + alarm1 |= uint32(uint8ToBCD(uint8(dt.Hour()))|A1M3) << 8 + alarm1 |= uint32(uint8ToBCD(uint8(day)) | A1M4 | DY_DT) + + if err := d.d.Write32(REG_ALARMONE, alarm1); err != nil { + return err + } + + control |= AlarmFlag_Alarm1 + return d.d.Write8(REG_CONTROL, control) +} + +// ReadAlarm1 returns the alarm1 time +func (d *Device) ReadAlarm1() (dt time.Time, err error) { + data := make([]uint8, 4) + if err = d.d.ReadData(REG_ALARMONE, data); err != nil { + return + } + second := bcdToInt(data[0] & 0x7F) + minute := bcdToInt(data[1] & 0x7F) + hour := hoursBCDToInt(data[2] & 0x3F) + + isDayOfWeek := (data[3] & 0x40) >> 6 + var day int + if isDayOfWeek > 0 { + day = bcdToInt(data[3] & 0x0F) + } else { + day = bcdToInt(data[3] & 0x3F) + } + + dt = time.Date(2000, 5, day, hour, minute, second, 0, time.UTC) + return +} + +// SetAlarm2 sets alarm2 to the given time and mode +func (d *Device) SetAlarm2(dt time.Time, mode Alarm2Mode) error { + control, err := d.d.Read8(REG_CONTROL) + if err != nil { + return err + } + if control&(1< 0 { + day = dowToDS3231(int(dt.Weekday())) + } + + data := make([]uint8, 4) + data[0] = uint8ToBCD(uint8(dt.Minute())) | A2M2 + data[1] = uint8ToBCD(uint8(dt.Hour())) | A2M3 + data[2] = uint8ToBCD(uint8(day)) | A2M4 | DY_DT + if err = d.bus.Tx(d.Address, append([]byte{REG_ALARMTWO}, data...), nil); err != nil { + return err + } + + control |= AlarmFlag_Alarm2 + return d.d.Write8(REG_CONTROL, control) +} + +// ReadAlarm2 returns the alarm2 time +func (d *Device) ReadAlarm2() (dt time.Time, err error) { + data := make([]uint8, 3) + if err = d.d.ReadData(REG_ALARMTWO, data); err != nil { + return + } + minute := bcdToInt(data[0] & 0x7F) + hour := hoursBCDToInt(data[1] & 0x3F) + + isDayOfWeek := (data[2] & 0x40) >> 6 + var day int + if isDayOfWeek > 0 { + day = bcdToInt(data[2] & 0x0F) + } else { + day = bcdToInt(data[2] & 0x3F) + } + + dt = time.Date(2000, 5, day, hour, minute, 0, 0, time.UTC) + return +} + +// IsEnabledAlarm1 returns true when alarm1 is enabled +func (d *Device) IsEnabledAlarm1() bool { + return d.isEnabledAlarm(1) +} + +// SetEnabledAlarm1 sets the enabled status of alarm1 +func (d *Device) SetEnabledAlarm1(enable bool) error { + if enable { + return d.enableAlarm(1) + } + return d.disableAlarm(1) +} + +// IsEnabledAlarm2 returns true when alarm2 is enabled +func (d *Device) IsEnabledAlarm2() bool { + return d.isEnabledAlarm(2) +} + +// SetEnabledAlarm2 sets the enabled status of alarm2 +func (d *Device) SetEnabledAlarm2(enable bool) error { + if enable { + return d.enableAlarm(2) + } + return d.disableAlarm(2) +} + +// ClearAlarm1 clears status of alarm1 +func (d *Device) ClearAlarm1() error { + return d.clearAlarm(1) +} + +// ClearAlarm2 clears status of alarm2 +func (d *Device) ClearAlarm2() error { + return d.clearAlarm(2) +} + +// IsAlarm1Fired returns true when alarm1 is firing +func (d *Device) IsAlarm1Fired() bool { + return d.isAlarmFired(1) +} + +// IsAlarm2Fired returns true when alarm2 is firing +func (d *Device) IsAlarm2Fired() bool { + return d.isAlarmFired(2) +} + +// SetEnabled32K sets the enabled status of the 32KHz output +func (d *Device) SetEnabled32K(enable bool) error { + status, err := d.d.Read8(REG_STATUS) + if err != nil { + return err + } + + if enable { + status |= 1 << EN32KHZ + } else { + status &^= 1 << EN32KHZ + } + + return d.d.Write8(REG_STATUS, status) +} + +// IsEnabled32K returns true when the 32KHz output is enabled +func (d *Device) IsEnabled32K() bool { + status, err := d.d.Read8(REG_STATUS) + if err != nil { + return false + } + return (status & (1 << EN32KHZ)) != 0x00 +} + +func (d *Device) disableAlarm(alarm_num uint8) error { + control, err := d.d.Read8(REG_CONTROL) + if err != nil { + return err + } + control &^= (1 << (alarm_num - 1)) + return d.d.Write8(REG_CONTROL, control) +} + +func (d *Device) enableAlarm(alarm_num uint8) error { + control, err := d.d.Read8(REG_CONTROL) + if err != nil { + return err + } + control |= (1 << (alarm_num - 1)) + return d.d.Write8(REG_CONTROL, control) +} + +func (d *Device) isEnabledAlarm(alarm_num uint8) bool { + control, err := d.d.Read8(REG_CONTROL) + if err != nil { + return false + } + return (control & (1 << (alarm_num - 1))) != 0x00 +} + +func (d *Device) clearAlarm(alarm_num uint8) error { + status, err := d.d.Read8(REG_STATUS) + if err != nil { + return err + } + status &^= (1 << (alarm_num - 1)) + return d.d.Write8(REG_STATUS, status) +} + +func (d *Device) isAlarmFired(alarm_num uint8) bool { + status, err := d.d.Read8(REG_STATUS) + if err != nil { + return false + } + return (status & (1 << (alarm_num - 1))) != 0x00 } // milliCelsius converts the raw temperature bytes (msb and lsb) from the DS3231 @@ -172,8 +416,8 @@ func (d *Device) ReadTemperature() (int32, error) { // 16-bit signed integer in units of centi Celsius (1/100 deg C) with no loss of // precision or dynamic range. But for backwards compatibility, let's instead // convert this into a 32-bit signed integer in units of milli Celsius. -func milliCelsius(msb uint8, lsb uint8) int32 { - t256 := int16(uint16(msb)<<8 | uint16(lsb)) +func milliCelsius(tempBytes uint16) int32 { + t256 := int16(uint16(tempBytes>>8)<<8 | uint16(tempBytes&0xFF)) t1000 := int32(t256) / 64 * 250 return t1000 } @@ -200,3 +444,11 @@ func hoursBCDToInt(value uint8) (hour int) { } return } + +// dowToDS3231 converts the day of the week to internal DS3231 format +func dowToDS3231(d int) int { + if d == 0 { + return 7 + } + return d +} diff --git a/ds3231/ds3231_test.go b/ds3231/ds3231_test.go index 91ba3c7..9b174db 100644 --- a/ds3231/ds3231_test.go +++ b/ds3231/ds3231_test.go @@ -5,71 +5,71 @@ import ( ) func TestPositiveMilliCelsius(t *testing.T) { - t1000 := milliCelsius(0, 0) + t1000 := milliCelsius(0) if t1000 != 0 { t.Fatal(t1000) } - t1000 = milliCelsius(0, 0b01000000) + t1000 = milliCelsius(0b0000000001000000) if t1000 != 250 { t.Fatal(t1000) } - t1000 = milliCelsius(0, 0b10000000) + t1000 = milliCelsius(0b0000000010000000) if t1000 != 500 { t.Fatal(t1000) } - t1000 = milliCelsius(0, 0b11000000) + t1000 = milliCelsius(0b0000000011000000) if t1000 != 750 { t.Fatal(t1000) } - t1000 = milliCelsius(1, 0b00000000) + t1000 = milliCelsius(0b0000000100000000) if t1000 != 1000 { t.Fatal(t1000) } - t1000 = milliCelsius(2, 0b00000000) + t1000 = milliCelsius(0b0000001000000000) if t1000 != 2000 { t.Fatal(t1000) } // highest temperature is 127.750C - t1000 = milliCelsius(0x7f, 0b11000000) + t1000 = milliCelsius(0b0111111111000000) if t1000 != 127750 { t.Fatal(t1000) } } func TestNegativeMilliCelsius(t *testing.T) { - t1000 := milliCelsius(0xff, 0b11000000) + t1000 := milliCelsius(0b1111111111000000) if t1000 != -250 { t.Fatal(t1000) } - t1000 = milliCelsius(0xff, 0b10000000) + t1000 = milliCelsius(0b1111111110000000) if t1000 != -500 { t.Fatal(t1000) } - t1000 = milliCelsius(0xff, 0b01000000) + t1000 = milliCelsius(0b1111111101000000) if t1000 != -750 { t.Fatal(t1000) } - t1000 = milliCelsius(0xff, 0b00000000) + t1000 = milliCelsius(0b1111111100000000) if t1000 != -1000 { t.Fatal(t1000) } - t1000 = milliCelsius(0xfe, 0b00000000) + t1000 = milliCelsius(0b1111111000000000) if t1000 != -2000 { t.Fatal(t1000) } // lowest temperature is -128.000C - t1000 = milliCelsius(0x80, 0b00000000) + t1000 = milliCelsius(0b1000000000000000) if t1000 != -128000 { t.Fatal(t1000) } diff --git a/ds3231/registers.go b/ds3231/registers.go index 05f1760..f38b701 100644 --- a/ds3231/registers.go +++ b/ds3231/registers.go @@ -46,3 +46,52 @@ const ( AlarmTwo Mode = 4 ModeAlarmBoth Mode = 5 ) + +// SQW Pin Modes +type SqwPinMode uint8 + +const ( + SQW_OFF SqwPinMode = 0x1C + SQW_1HZ SqwPinMode = 0x00 + SQW_1KHZ SqwPinMode = 0x08 + SQW_4KHZ SqwPinMode = 0x10 + SQW_8KHZ SqwPinMode = 0x18 +) + +// Alarm1 Modes define which parts of the set alarm time has to match the current timestamp of the clock device for +// alarm1 to fire +type Alarm1Mode uint8 + +const ( + // Alarm1 fires every second + A1_PER_SECOND Alarm1Mode = 0x0F + // Alarm1 fires when the seconds match + A1_SECOND Alarm1Mode = 0x0E + // Alarm1 fires when both seconds and minutes match + A1_MINUTE Alarm1Mode = 0x0C + // Alarm1 fires when seconds, minutes and hours match + A1_HOUR Alarm1Mode = 0x08 + // Alarm1 fires when seconds, minutes, hours and the day of the month match + A1_DATE Alarm1Mode = 0x00 + // Alarm1 fires when seconds, minutes, hours and the day of the week match + A1_DAY Alarm1Mode = 0x10 +) + +// Alarm2 Modes define which parts of the set alarm time has to match the current timestamp of the clock device for +// alarm2 to fire. +// +// Alarm2 only supports matching down to the minute unlike alarm1 which supports matching down to the second. +type Alarm2Mode uint8 + +const ( + // Alarm2 fires every minute + A2_PER_MINUTE Alarm2Mode = 0x07 + // Alarm2 fires when the minutes match + A2_MINUTE Alarm2Mode = 0x06 + // Alarm2 fires when both minutes and hours match + A2_HOUR Alarm2Mode = 0x04 + // Alarm2 fires when minutes, hours and the day of the month match + A2_DATE Alarm2Mode = 0x00 + // Alarm2 fires when minutes, hours and the day of the week match + A2_DAY Alarm2Mode = 0x08 +) diff --git a/examples/ds3231/alarms/main.go b/examples/ds3231/alarms/main.go new file mode 100644 index 0000000..7ef9e87 --- /dev/null +++ b/examples/ds3231/alarms/main.go @@ -0,0 +1,74 @@ +// Connects to an DS3231 I2C Real Time Clock (RTC) and sets both alarms. It then repeatedly checks +// if the alarms are firing and prints out a message if that is the case. +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/ds3231" +) + +func main() { + machine.I2C0.Configure(machine.I2CConfig{}) + + rtc := ds3231.New(machine.I2C0) + rtc.Configure() + + valid := rtc.IsTimeValid() + if !valid { + date := time.Date(2019, 12, 05, 20, 34, 12, 0, time.UTC) + rtc.SetTime(date) + } + + // Set alarm1 so it triggers when the seconds match 59 => repeats every minute at dd:hh:mm:59 + if err := rtc.SetAlarm1(time.Date(0, 0, 0, 0, 0, 59, 0, time.UTC), ds3231.A1_SECOND); err != nil { + println("Error while setting Alarm1") + } + if err := rtc.SetEnabledAlarm1(true); err != nil { + println("Error while enabling Alarm1") + } + + // Set alarm2 so it triggers when the minutes match 35 => repeats every hour at dd:hh:35:ss + if err := rtc.SetAlarm2(time.Date(0, 0, 0, 0, 35, 0, 0, time.UTC), ds3231.A2_MINUTE); err != nil { + println("Error while setting Alarm2") + } + if err := rtc.SetEnabledAlarm2(true); err != nil { + println("Error while enabling Alarm2") + } + + running := rtc.IsRunning() + if !running { + err := rtc.SetRunning(true) + if err != nil { + println("Error configuring RTC") + } + } + + for { + dt, err := rtc.ReadTime() + if err != nil { + println("Error reading date:", err) + continue + } + + a1 := rtc.IsAlarm1Fired() + a2 := rtc.IsAlarm2Fired() + + println(dt.Format(time.DateTime), "A1:", a1, "A2:", a2) + + if a1 { + if err := rtc.ClearAlarm1(); err != nil { + println("Error while clearing alarm1") + } + } + if a2 { + if err := rtc.ClearAlarm2(); err != nil { + println("Error while clearing alarm2") + } + + } + + time.Sleep(time.Second * 1) + } +} diff --git a/examples/ds3231/main.go b/examples/ds3231/basic/main.go similarity index 68% rename from examples/ds3231/main.go rename to examples/ds3231/basic/main.go index 08ec468..e406176 100644 --- a/examples/ds3231/main.go +++ b/examples/ds3231/basic/main.go @@ -3,10 +3,9 @@ package main import ( "machine" + "strconv" "time" - "fmt" - "tinygo.org/x/drivers/ds3231" ) @@ -26,19 +25,19 @@ func main() { if !running { err := rtc.SetRunning(true) if err != nil { - fmt.Println("Error configuring RTC") + println("Error configuring RTC") } } for { dt, err := rtc.ReadTime() if err != nil { - fmt.Println("Error reading date:", err) + println("Error reading date:", err) } else { - fmt.Printf("Date: %d/%s/%02d %02d:%02d:%02d \r\n", dt.Year(), dt.Month(), dt.Day(), dt.Hour(), dt.Minute(), dt.Second()) + println(dt.Format(time.DateTime)) } temp, _ := rtc.ReadTemperature() - fmt.Printf("Temperature: %.2f °C \r\n", float32(temp)/1000) + println("Temperature:", strconv.FormatFloat(float64(temp)/1000, 'f', -1, 32), "°C") time.Sleep(time.Second * 1) } diff --git a/smoketest.sh b/smoketest.sh index ff10692..6d6975a 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -22,7 +22,8 @@ tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmp tinygo build -size short -o ./build/test.hex -target=trinket-m0 ./examples/bmp388/main.go tinygo build -size short -o ./build/test.hex -target=bluepill ./examples/ds1307/sram/main.go tinygo build -size short -o ./build/test.hex -target=bluepill ./examples/ds1307/time/main.go -tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/ds3231/main.go +tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/ds3231/alarms/main.go +tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/ds3231/basic/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/easystepper/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/flash/console/spi tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/flash/console/qspi From 09fd01340b63ea9ce2ec72f07d54eb7e05aeb380 Mon Sep 17 00:00:00 2001 From: Patricio Whittingslow Date: Mon, 10 Nov 2025 19:20:35 -0300 Subject: [PATCH 098/137] add simplest driver ports --- apa102/apa102.go | 10 ++++++--- apa102/softspi.go | 18 ++++++++++----- bmi160/bmi160.go | 55 ++++++++++++++++++++++++++-------------------- buzzer/buzzer.go | 14 ++++++------ ft6336/ft6336.go | 3 +++ hd44780/hd44780.go | 10 ++++----- max6675/max6675.go | 8 +++---- max72xx/max72xx.go | 23 +++++++++++-------- mcp2515/mcp2515.go | 24 +++++++++++++------- pcd8544/pcd8544.go | 16 +++++++------- 10 files changed, 107 insertions(+), 74 deletions(-) diff --git a/apa102/apa102.go b/apa102/apa102.go index 98253bf..0cc22e0 100644 --- a/apa102/apa102.go +++ b/apa102/apa102.go @@ -5,9 +5,10 @@ package apa102 // import "tinygo.org/x/drivers/apa102" import ( "image/color" - "machine" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/internal/pin" ) const ( @@ -37,8 +38,11 @@ func New(b drivers.SPI) *Device { // NewSoftwareSPI returns a new APA102 driver that will use a software based // implementation of the SPI protocol. -func NewSoftwareSPI(sckPin, sdoPin machine.Pin, delay uint32) *Device { - return New(&bbSPI{SCK: sckPin, SDO: sdoPin, Delay: delay}) +func NewSoftwareSPI(sckPin, sdoPin pin.Output, delay uint32) *Device { + return New(&bbSPI{SCK: sckPin.Set, SDO: sdoPin.Set, Delay: delay, configurePins: func() { + legacy.ConfigurePinOut(sckPin) + legacy.ConfigurePinOut(sdoPin) + }}) } // WriteColors writes the given RGBA color slice out using the APA102 protocol. diff --git a/apa102/softspi.go b/apa102/softspi.go index 89e8a85..113136c 100644 --- a/apa102/softspi.go +++ b/apa102/softspi.go @@ -1,6 +1,9 @@ package apa102 -import "machine" +import ( + "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/internal/pin" +) // bbSPI is a dumb bit-bang implementation of SPI protocol that is hardcoded // to mode 0 and ignores trying to receive data. Just enough for the APA102. @@ -8,15 +11,18 @@ import "machine" // most purposes other than the APA102 package. It might be desirable to make // this more generic and include it in the TinyGo "machine" package instead. type bbSPI struct { - SCK machine.Pin - SDO machine.Pin - Delay uint32 + SCK pin.OutputFunc + SDO pin.OutputFunc + Delay uint32 + configurePins func() } // Configure sets up the SCK and SDO pins as outputs and sets them low func (s *bbSPI) Configure() { - s.SCK.Configure(machine.PinConfig{Mode: machine.PinOutput}) - s.SDO.Configure(machine.PinConfig{Mode: machine.PinOutput}) + if s.configurePins == nil { + panic(legacy.ErrConfigBeforeInstantiated) + } + s.configurePins() s.SCK.Low() s.SDO.Low() if s.Delay == 0 { diff --git a/bmi160/bmi160.go b/bmi160/bmi160.go index cd3e882..71e602e 100644 --- a/bmi160/bmi160.go +++ b/bmi160/bmi160.go @@ -1,31 +1,36 @@ package bmi160 import ( - "machine" "time" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/internal/pin" ) // DeviceSPI is the SPI interface to a BMI160 accelerometer/gyroscope. There is // also an I2C interface, but it is not yet supported. type DeviceSPI struct { // Chip select pin - CSB machine.Pin + csb pin.OutputFunc buf [7]byte // SPI bus (requires chip select to be usable). - Bus drivers.SPI + bus drivers.SPI + configurePins func() } // NewSPI returns a new device driver. The pin and SPI interface are not // touched, provide a fully configured SPI object and call Configure to start // using this device. -func NewSPI(csb machine.Pin, spi drivers.SPI) *DeviceSPI { +func NewSPI(csb pin.Output, spi drivers.SPI) *DeviceSPI { return &DeviceSPI{ - CSB: csb, // chip select - Bus: spi, + csb: csb.Set, // chip select + bus: spi, + configurePins: func() { + legacy.ConfigurePinOut(csb) + }, } } @@ -33,9 +38,11 @@ func NewSPI(csb machine.Pin, spi drivers.SPI) *DeviceSPI { // configures the BMI160, but it does not configure the SPI interface (it is // assumed to be up and running). func (d *DeviceSPI) Configure() error { - d.CSB.Configure(machine.PinConfig{Mode: machine.PinOutput}) - d.CSB.High() - + if d.configurePins == nil { + return legacy.ErrConfigBeforeInstantiated + } + d.configurePins() + d.csb.High() // The datasheet recommends doing a register read from address 0x7F to get // SPI communication going: // > If CSB sees a rising edge after power-up, the BMI160 interface switches @@ -86,9 +93,9 @@ func (d *DeviceSPI) ReadTemperature() (temperature int32, err error) { data[0] = 0x80 | reg_TEMPERATURE_0 data[1] = 0 data[2] = 0 - d.CSB.Low() - err = d.Bus.Tx(data, data) - d.CSB.High() + d.csb.Low() + err = d.bus.Tx(data, data) + d.csb.High() if err != nil { return } @@ -123,9 +130,9 @@ func (d *DeviceSPI) ReadAcceleration() (x int32, y int32, z int32, err error) { for i := 1; i < len(data); i++ { data[i] = 0 } - d.CSB.Low() - err = d.Bus.Tx(data, data) - d.CSB.High() + d.csb.Low() + err = d.bus.Tx(data, data) + d.csb.High() if err != nil { return } @@ -153,9 +160,9 @@ func (d *DeviceSPI) ReadRotation() (x int32, y int32, z int32, err error) { for i := 1; i < len(data); i++ { data[i] = 0 } - d.CSB.Low() - err = d.Bus.Tx(data, data) - d.CSB.High() + d.csb.Low() + err = d.bus.Tx(data, data) + d.csb.High() if err != nil { return } @@ -201,9 +208,9 @@ func (d *DeviceSPI) readRegister(address uint8) uint8 { data := d.buf[:2] data[0] = 0x80 | address data[1] = 0 - d.CSB.Low() - d.Bus.Tx(data, data) - d.CSB.High() + d.csb.Low() + d.bus.Tx(data, data) + d.csb.High() return data[1] } @@ -217,7 +224,7 @@ func (d *DeviceSPI) writeRegister(address, data uint8) { buf[0] = address buf[1] = data - d.CSB.Low() - d.Bus.Tx(buf, buf) - d.CSB.High() + d.csb.Low() + d.bus.Tx(buf, buf) + d.csb.High() } diff --git a/buzzer/buzzer.go b/buzzer/buzzer.go index 04c1129..11b054d 100644 --- a/buzzer/buzzer.go +++ b/buzzer/buzzer.go @@ -2,22 +2,22 @@ package buzzer // import "tinygo.org/x/drivers/buzzer" import ( - "machine" - "time" + + "tinygo.org/x/drivers/internal/pin" ) // Device wraps a GPIO connection to a buzzer. type Device struct { - pin machine.Pin + pin pin.OutputFunc High bool BPM float64 } // New returns a new buzzer driver given which pin to use -func New(pin machine.Pin) Device { +func New(pin pin.Output) Device { return Device{ - pin: pin, + pin: pin.Set, High: false, BPM: 96.0, } @@ -25,14 +25,14 @@ func New(pin machine.Pin) Device { // On sets the buzzer to a high state. func (l *Device) On() (err error) { - l.pin.Set(true) + l.pin.High() l.High = true return } // Off sets the buzzer to a low state. func (l *Device) Off() (err error) { - l.pin.Set(false) + l.pin.Low() l.High = false return } diff --git a/ft6336/ft6336.go b/ft6336/ft6336.go index 6b2032d..b16dc27 100644 --- a/ft6336/ft6336.go +++ b/ft6336/ft6336.go @@ -1,3 +1,6 @@ +// Guarded because still unsure of how to deal with interrupt drivers. +//go:build tinygo + // Package ft6336 provides a driver for the FT6336 I2C Self-Capacitive touch // panel controller. // diff --git a/hd44780/hd44780.go b/hd44780/hd44780.go index 111f70d..2cf2c0a 100644 --- a/hd44780/hd44780.go +++ b/hd44780/hd44780.go @@ -210,7 +210,7 @@ func (d *Device) SendCommand(command byte) { d.bus.SetCommandMode(true) d.bus.Write([]byte{command}) - for d.busy(command == DISPLAY_CLEAR || command == CURSOR_HOME) { + for d.isBusy(command == DISPLAY_CLEAR || command == CURSOR_HOME) { } } @@ -219,7 +219,7 @@ func (d *Device) sendData(data byte) { d.bus.SetCommandMode(false) d.bus.Write([]byte{data}) - for d.busy(false) { + for d.isBusy(false) { } } @@ -231,9 +231,9 @@ func (d *Device) CreateCharacter(cgramAddr uint8, data []byte) { } } -// busy returns true when hd447890 is busy +// isBusy returns true when hd447890 is isBusy // or after the timeout specified -func (d *Device) busy(longDelay bool) bool { +func (d *Device) isBusy(longDelay bool) bool { if d.bus.WriteOnly() { // Can't read busy flag if write only, so sleep a bit then return if longDelay { @@ -261,7 +261,7 @@ func (d *Device) busy(longDelay bool) bool { // Busy returns true when hd447890 is busy func (d *Device) Busy() bool { - return d.busy(false) + return d.isBusy(false) } // Size returns the current size of the display. diff --git a/max6675/max6675.go b/max6675/max6675.go index eb67596..15ecf7c 100644 --- a/max6675/max6675.go +++ b/max6675/max6675.go @@ -3,9 +3,9 @@ package max6675 import ( "errors" - "machine" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/pin" ) // ErrThermocoupleOpen is returned when the thermocouple input is open. @@ -14,16 +14,16 @@ var ErrThermocoupleOpen = errors.New("thermocouple input open") type Device struct { bus drivers.SPI - cs machine.Pin + cs pin.OutputFunc } // Create a new Device to read from a MAX6675 thermocouple. // Pins must be configured before use. Frequency for SPI // should be 4.3MHz maximum. -func NewDevice(bus drivers.SPI, cs machine.Pin) *Device { +func NewDevice(bus drivers.SPI, cs pin.Output) *Device { return &Device{ bus: bus, - cs: cs, + cs: cs.Set, } } diff --git a/max72xx/max72xx.go b/max72xx/max72xx.go index ca6c819..82cceac 100644 --- a/max72xx/max72xx.go +++ b/max72xx/max72xx.go @@ -3,31 +3,36 @@ package max72xx import ( - "machine" - "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/internal/pin" ) type Device struct { - bus drivers.SPI - cs machine.Pin + bus drivers.SPI + cs pin.OutputFunc + configurePins func() } // NewDriver creates a new max7219 connection. The SPI wire must already be configured // The SPI frequency must not be higher than 10MHz. // parameter cs: the datasheet also refers to this pin as "load" pin. -func NewDevice(bus drivers.SPI, cs machine.Pin) *Device { +func NewDevice(bus drivers.SPI, cs pin.Output) *Device { return &Device{ bus: bus, - cs: cs, + cs: cs.Set, + configurePins: func() { + legacy.ConfigurePinOut(cs) + }, } } // Configure setups the pins. func (driver *Device) Configure() { - outPutConfig := machine.PinConfig{Mode: machine.PinOutput} - - driver.cs.Configure(outPutConfig) + if driver.configurePins == nil { + panic(legacy.ErrConfigBeforeInstantiated) + } + driver.configurePins() } // SetScanLimit sets the scan limit. Maximum is 8. diff --git a/mcp2515/mcp2515.go b/mcp2515/mcp2515.go index 2b636b1..ac08b5a 100644 --- a/mcp2515/mcp2515.go +++ b/mcp2515/mcp2515.go @@ -8,18 +8,20 @@ package mcp2515 // import "tinygo.org/x/drivers/mcp2515" import ( "errors" "fmt" - "machine" "time" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/internal/pin" ) // Device wraps MCP2515 SPI CAN Module. type Device struct { - spi SPI - cs machine.Pin - msg *CANMsg - mcpMode byte + spi SPI + cs pin.OutputFunc + msg *CANMsg + mcpMode byte + configurePins func() } // CANMsg stores CAN message fields. @@ -36,15 +38,18 @@ const ( ) // New returns a new MCP2515 driver. Pass in a fully configured SPI bus. -func New(b drivers.SPI, csPin machine.Pin) *Device { +func New(b drivers.SPI, csPin pin.Output) *Device { d := &Device{ spi: SPI{ bus: b, tx: make([]byte, 0, bufferSize), rx: make([]byte, 0, bufferSize), }, - cs: csPin, + cs: csPin.Set, msg: &CANMsg{}, + configurePins: func() { + legacy.ConfigurePinOut(csPin) + }, } return d @@ -52,7 +57,10 @@ func New(b drivers.SPI, csPin machine.Pin) *Device { // Configure sets up the device for communication. func (d *Device) Configure() { - d.cs.Configure(machine.PinConfig{Mode: machine.PinOutput}) + if d.configurePins == nil { + panic(legacy.ErrConfigBeforeInstantiated) + } + d.configurePins() } const beginTimeoutValue int = 10 diff --git a/pcd8544/pcd8544.go b/pcd8544/pcd8544.go index 3a843cb..447492b 100644 --- a/pcd8544/pcd8544.go +++ b/pcd8544/pcd8544.go @@ -6,18 +6,18 @@ package pcd8544 // import "tinygo.org/x/drivers/pcd8544" import ( "errors" "image/color" - "machine" "time" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/pin" ) // Device wraps an SPI connection. type Device struct { bus drivers.SPI - dcPin machine.Pin - rstPin machine.Pin - scePin machine.Pin + dcPin pin.OutputFunc + rstPin pin.OutputFunc + scePin pin.OutputFunc buffer []byte width int16 height int16 @@ -30,12 +30,12 @@ type Config struct { } // New creates a new PCD8544 connection. The SPI bus must already be configured. -func New(bus drivers.SPI, dcPin, rstPin, scePin machine.Pin) *Device { +func New(bus drivers.SPI, dcPin, rstPin, scePin pin.Output) *Device { return &Device{ bus: bus, - dcPin: dcPin, - rstPin: rstPin, - scePin: scePin, + dcPin: dcPin.Set, + rstPin: rstPin.Set, + scePin: scePin.Set, } } From 4b831af52bc89403739e75f80c7dba99746a9f4c Mon Sep 17 00:00:00 2001 From: Nicholas Wiersma Date: Wed, 12 Nov 2025 10:00:21 +0200 Subject: [PATCH 099/137] w5500: initial version the driver (#788) Thank you, @nrwiersma for working on this and @soypat for review. Now merging. --- examples/w5500/main.go | 37 ++++ smoketest.sh | 1 + w5500/io.go | 104 ++++++++++ w5500/netdev.go | 445 +++++++++++++++++++++++++++++++++++++++++ w5500/registers.go | 88 ++++++++ w5500/w5500.go | 248 +++++++++++++++++++++++ 6 files changed, 923 insertions(+) create mode 100644 examples/w5500/main.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 0000000..ea0a5b6 --- /dev/null +++ b/examples/w5500/main.go @@ -0,0 +1,37 @@ +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(machine.SPI0, machine.GPIO17) + eth.Configure(w5500.Config{ + 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 6d6975a..29b3ce3 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -146,6 +146,7 @@ tinygo build -size short -o ./build/test.uf2 -target=nicenano ./examples/sharpme 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/si5351/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/io.go b/w5500/io.go new file mode 100644 index 0000000..479c06c --- /dev/null +++ b/w5500/io.go @@ -0,0 +1,104 @@ +package w5500 + +import "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 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) + + 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 + _ = 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 0000000..d64a16a --- /dev/null +++ b/w5500/netdev.go @@ -0,0 +1,445 @@ +package w5500 + +import ( + "errors" + "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) 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 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, errors.New("unsupported combination of socket type and 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 { + // The IP address is irrelevant. The configured ip will always be used. + port := ip.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 errors.New("could not set socket port: " + err.Error()) + } + + 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 errors.New("socket is closed after binding") + } + 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 { + destIP := ip.Addr() + if host != "" { + var err error + destIP, err = d.GetHostByName(host) + if err != nil { + return errors.New("could not resolve host " + host + ":" + err.Error()) + } + } + if !destIP.IsValid() || !destIP.Is4() { + return errors.New("invalid destination IP address: " + destIP.String()) + } + port := ip.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 { + 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 errors.New("not a TCP socket") + } + + if err = d.listen(sock.sockn); err != nil { + return errors.New("could not send listen command: " + err.Error()) + } + return nil +} + +func (d *Device) listen(sockn uint8) error { + state := d.sockStatus(sockn) + if state != sockStatusInit { + return errors.New("socket is not in the initial 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) { + d.mu.Lock() + defer d.mu.Unlock() + + lsock, err := d.socket(sockfd) + if err != nil { + return -1, netip.AddrPort{}, errors.New("could not get socket: " + err.Error()) + } + + 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{}, errors.New("could not bind listening socket: " + err.Error()) + } + if err = d.listen(lsock.sockn); err != nil { + return -1, netip.AddrPort{}, errors.New("could not set listening socket: " + err.Error()) + } + + csock.setInUse(true) + + 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 errors.New("could not set socket to listen: " + err.Error()) + } + } + + 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) { + 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, errors.New("could not send chunk: " + err.Error()) + } + 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, errors.New("could not get socket: " + err.Error()) + } + 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 errors.New("socket is not in a valid state for sending data") + } + + 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) { + d.mu.Lock() + defer d.mu.Unlock() + + sock, err := d.socket(sockfd) + if err != nil { + return 0, errors.New("could not get socket: " + err.Error()) + } + 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 { + 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 0000000..0ff1b09 --- /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 0000000..d63c812 --- /dev/null +++ b/w5500/w5500.go @@ -0,0 +1,248 @@ +// Package w5500 implements a driver for the W5500 Ethernet controller. +// +// The driver supports basic network functionality including TCP and UDP sockets. +// It currently does not use the IRQ or RST pins. +// +// Datasheet: https://docs.wiznet.io/img/products/w5500/W5500_ds_v110e.pdf +// Product Page: https://wiznet.io/products/ethernet-chips/w5500 +package w5500 + +import ( + "errors" + "net" + "net/netip" + "sync" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/pin" + "tinygo.org/x/drivers/netdev" +) + +var _ netdev.Netdever = &Device{} + +// Resolver is a function that resolves a hostname to an IP address. +type Resolver func(host string) (netip.Addr, error) + +// Device is a driver for the W5500 Ethernet controller. +type Device struct { + maxSockets int + maxSockSize int + + mu sync.Mutex + bus drivers.SPI + cs pin.OutputFunc + dns Resolver + + sockets []*socket + laddr netip.Addr + + cmdBuf [3]byte +} + +// New returns a new w5500 driver. +func New(bus drivers.SPI, csPin pin.Output) *Device { + return &Device{ + bus: bus, + cs: csPin.Set, + } +} + +// Config is the configuration for the device. +// +// The SPI bus must be fully configured. +type Config struct { + 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 { + d.cs(true) + + d.mu.Lock() + defer d.mu.Unlock() + + d.dns = cfg.DNS + + d.reset() + + if err := d.setupSockets(cfg.MaxSockets); err != nil { + return errors.New("could not setup sockets: " + err.Error()) + } + + // 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 errors.New("invalid number of sockets, must be one of 1, 2, 4, or 8") + } + + 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 +} + +// 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) { + 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 errors.New("could not set IP address: " + err.Error()) + } + + 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 errors.New("invalid IP address: " + ip.String()) + } + + 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 c710cc82369f88acbd40312236f679b7998bfe9a Mon Sep 17 00:00:00 2001 From: Yurii Soldak Date: Wed, 12 Nov 2025 09:40:34 +0100 Subject: [PATCH 100/137] ssd1xxx: break dependency from machine package (#812) * ssd1xxx: break dependency from machine package * fix ssd1289 example * simplify --- examples/ssd1289/main.go | 3 ++- ssd1289/pinbus.go | 31 ++++++++-------------- ssd1289/ssd1289.go | 39 +++++++++++++++------------- ssd1306/ssd1306_spi.go | 26 ++++++++++--------- ssd1331/ssd1331.go | 26 ++++++++++--------- ssd1351/ssd1351.go | 55 ++++++++++++++++++++++------------------ 6 files changed, 92 insertions(+), 88 deletions(-) diff --git a/examples/ssd1289/main.go b/examples/ssd1289/main.go index c68f328..96a2176 100644 --- a/examples/ssd1289/main.go +++ b/examples/ssd1289/main.go @@ -5,6 +5,7 @@ import ( "machine" "math/rand" + "tinygo.org/x/drivers/internal/pin" "tinygo.org/x/drivers/ssd1289" ) @@ -16,7 +17,7 @@ func main() { //consider creating a more efficient bus implementation that uses //your microcontrollers built in "ports" //see rp2040bus.go for an example for the rapsberry pi pico - bus := ssd1289.NewPinBus([16]machine.Pin{ + bus := ssd1289.NewPinBus([16]pin.Output{ machine.GP4, //DB0 machine.GP5, //DB1 machine.GP6, //DB2 diff --git a/ssd1289/pinbus.go b/ssd1289/pinbus.go index aacdf79..7dde7b6 100644 --- a/ssd1289/pinbus.go +++ b/ssd1289/pinbus.go @@ -1,15 +1,19 @@ package ssd1289 -import "machine" +import ( + "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/internal/pin" +) type pinBus struct { - pins [16]machine.Pin + pins [16]pin.Output } -func NewPinBus(pins [16]machine.Pin) pinBus { +func NewPinBus(pins [16]pin.Output) pinBus { + // configure GPIO pins (on baremetal targets only, for backwards compatibility) for i := 0; i < 16; i++ { - pins[i].Configure(machine.PinConfig{Mode: machine.PinOutput}) + legacy.ConfigurePinOut(pins[i]) } return pinBus{ @@ -18,20 +22,7 @@ func NewPinBus(pins [16]machine.Pin) pinBus { } func (b pinBus) Set(data uint16) { - b.pins[15].Set((data & (1 << 15)) != 0) - b.pins[14].Set((data & (1 << 14)) != 0) - b.pins[13].Set((data & (1 << 13)) != 0) - b.pins[12].Set((data & (1 << 12)) != 0) - b.pins[11].Set((data & (1 << 11)) != 0) - b.pins[10].Set((data & (1 << 10)) != 0) - b.pins[9].Set((data & (1 << 9)) != 0) - b.pins[8].Set((data & (1 << 8)) != 0) - b.pins[7].Set((data & (1 << 7)) != 0) - b.pins[6].Set((data & (1 << 6)) != 0) - b.pins[5].Set((data & (1 << 5)) != 0) - b.pins[4].Set((data & (1 << 4)) != 0) - b.pins[3].Set((data & (1 << 3)) != 0) - b.pins[2].Set((data & (1 << 2)) != 0) - b.pins[1].Set((data & (1 << 1)) != 0) - b.pins[0].Set((data & (1 << 0)) != 0) + for i := 15; i >= 0; i-- { + b.pins[i].Set((data & (1 << i)) != 0) + } } diff --git a/ssd1289/ssd1289.go b/ssd1289/ssd1289.go index f5cc22a..8b1e55f 100644 --- a/ssd1289/ssd1289.go +++ b/ssd1289/ssd1289.go @@ -5,8 +5,10 @@ package ssd1289 import ( "image/color" - "machine" "time" + + "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/internal/pin" ) type Bus interface { @@ -14,33 +16,34 @@ type Bus interface { } type Device struct { - rs machine.Pin - wr machine.Pin - cs machine.Pin - rst machine.Pin + rs pin.OutputFunc + wr pin.OutputFunc + cs pin.OutputFunc + rst pin.OutputFunc bus Bus } const width = int16(240) const height = int16(320) -func New(rs machine.Pin, wr machine.Pin, cs machine.Pin, rst machine.Pin, bus Bus) Device { - d := Device{ - rs: rs, - wr: wr, - cs: cs, - rst: rst, +func New(rs, wr, cs, rst pin.Output, bus Bus) *Device { + d := &Device{ + rs: rs.Set, + wr: wr.Set, + cs: cs.Set, + rst: rst.Set, bus: bus, } - rs.Configure(machine.PinConfig{Mode: machine.PinOutput}) - wr.Configure(machine.PinConfig{Mode: machine.PinOutput}) - cs.Configure(machine.PinConfig{Mode: machine.PinOutput}) - rst.Configure(machine.PinConfig{Mode: machine.PinOutput}) + // configure GPIO pins (only on baremetal targets, for backwards compatibility) + legacy.ConfigurePinOut(rs) + legacy.ConfigurePinOut(wr) + legacy.ConfigurePinOut(cs) + legacy.ConfigurePinOut(rst) - cs.High() - rst.High() - wr.High() + d.cs.High() + d.rst.High() + d.wr.High() return d } diff --git a/ssd1306/ssd1306_spi.go b/ssd1306/ssd1306_spi.go index d96299d..938abce 100644 --- a/ssd1306/ssd1306_spi.go +++ b/ssd1306/ssd1306_spi.go @@ -1,31 +1,33 @@ package ssd1306 import ( - "machine" "time" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/internal/pin" ) type SPIBus struct { wire drivers.SPI - dcPin machine.Pin - resetPin machine.Pin - csPin machine.Pin + dcPin pin.OutputFunc + resetPin pin.OutputFunc + csPin pin.OutputFunc buffer []byte // buffer to avoid heap allocations } // NewSPI creates a new SSD1306 connection. The SPI wire must already be configured. -func NewSPI(bus drivers.SPI, dcPin, resetPin, csPin machine.Pin) *Device { - dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) +func NewSPI(bus drivers.SPI, dcPin, resetPin, csPin pin.Output) *Device { + // configure GPIO pins (on baremetal targets only, for backwards compatibility) + legacy.ConfigurePinOut(dcPin) + legacy.ConfigurePinOut(resetPin) + legacy.ConfigurePinOut(csPin) return &Device{ bus: &SPIBus{ wire: bus, - dcPin: dcPin, - resetPin: resetPin, - csPin: csPin, + dcPin: dcPin.Set, + resetPin: resetPin.Set, + csPin: csPin.Set, }, } } @@ -60,7 +62,7 @@ func (b *SPIBus) flush() error { // tx sends data to the display func (b *SPIBus) tx(data []byte, isCommand bool) error { b.csPin.High() - b.dcPin.Set(!isCommand) + b.dcPin(!isCommand) b.csPin.Low() err := b.wire.Tx(data, nil) b.csPin.High() diff --git a/ssd1331/ssd1331.go b/ssd1331/ssd1331.go index 0717c48..426850f 100644 --- a/ssd1331/ssd1331.go +++ b/ssd1331/ssd1331.go @@ -5,12 +5,13 @@ package ssd1331 // import "tinygo.org/x/drivers/ssd1331" import ( "image/color" - "machine" "errors" "time" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/internal/pin" ) type Model uint8 @@ -19,9 +20,9 @@ type Rotation uint8 // Device wraps an SPI connection. type Device struct { bus drivers.SPI - dcPin machine.Pin - resetPin machine.Pin - csPin machine.Pin + dcPin pin.OutputFunc + resetPin pin.OutputFunc + csPin pin.OutputFunc width int16 height int16 batchLength int16 @@ -36,15 +37,16 @@ type Config struct { } // New creates a new SSD1331 connection. The SPI wire must already be configured. -func New(bus drivers.SPI, resetPin, dcPin, csPin machine.Pin) Device { - dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) +func New(bus drivers.SPI, resetPin, dcPin, csPin pin.Output) Device { + // configure GPIO pins (on baremetal targets only, for backwards compatibility) + legacy.ConfigurePinOut(dcPin) + legacy.ConfigurePinOut(resetPin) + legacy.ConfigurePinOut(csPin) return Device{ bus: bus, - dcPin: dcPin, - resetPin: resetPin, - csPin: csPin, + dcPin: dcPin.Set, + resetPin: resetPin.Set, + csPin: csPin.Set, } } @@ -251,7 +253,7 @@ func (d *Device) Data(data uint8) { // Tx sends data to the display func (d *Device) Tx(data []byte, isCommand bool) { - d.dcPin.Set(!isCommand) + d.dcPin(!isCommand) d.bus.Tx(data, nil) } diff --git a/ssd1351/ssd1351.go b/ssd1351/ssd1351.go index 3561dd6..dd737f1 100644 --- a/ssd1351/ssd1351.go +++ b/ssd1351/ssd1351.go @@ -6,10 +6,11 @@ package ssd1351 // import "tinygo.org/x/drivers/ssd1351" import ( "errors" "image/color" - "machine" "time" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/internal/pin" ) var ( @@ -19,17 +20,18 @@ var ( // Device wraps an SPI connection. type Device struct { - bus drivers.SPI - dcPin machine.Pin - resetPin machine.Pin - csPin machine.Pin - enPin machine.Pin - rwPin machine.Pin - width int16 - height int16 - rowOffset int16 - columnOffset int16 - bufferLength int16 + bus drivers.SPI + dcPin pin.OutputFunc + resetPin pin.OutputFunc + csPin pin.OutputFunc + enPin pin.OutputFunc + rwPin pin.OutputFunc + width int16 + height int16 + rowOffset int16 + columnOffset int16 + bufferLength int16 + configurePins func() } // Config is the configuration for the display @@ -41,14 +43,21 @@ type Config struct { } // New creates a new SSD1351 connection. The SPI wire must already be configured. -func New(bus drivers.SPI, resetPin, dcPin, csPin, enPin, rwPin machine.Pin) Device { +func New(bus drivers.SPI, resetPin, dcPin, csPin, enPin, rwPin pin.Output) Device { return Device{ bus: bus, - dcPin: dcPin, - resetPin: resetPin, - csPin: csPin, - enPin: enPin, - rwPin: rwPin, + dcPin: dcPin.Set, + resetPin: resetPin.Set, + csPin: csPin.Set, + enPin: enPin.Set, + rwPin: rwPin.Set, + configurePins: func() { + legacy.ConfigurePinOut(dcPin) + legacy.ConfigurePinOut(resetPin) + legacy.ConfigurePinOut(csPin) + legacy.ConfigurePinOut(enPin) + legacy.ConfigurePinOut(rwPin) + }, } } @@ -72,12 +81,8 @@ func (d *Device) Configure(cfg Config) { d.bufferLength = d.height } - // configure GPIO pins - d.dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - d.resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - d.csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - d.enPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - d.rwPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + // configure GPIO pins (on baremetal targets only, for backwards compatibility) + d.configurePins() // reset the device d.resetPin.High() @@ -278,7 +283,7 @@ func (d *Device) Data(data uint8) { // Tx sends data to the display func (d *Device) Tx(data []byte, isCommand bool) { - d.dcPin.Set(!isCommand) + d.dcPin(!isCommand) d.csPin.Low() d.bus.Tx(data, nil) d.csPin.High() From 253f8e32208990dbbb79ab0abd54ae7e6e522fa2 Mon Sep 17 00:00:00 2001 From: sago35 Date: Sat, 15 Nov 2025 17:02:55 +0900 Subject: [PATCH 101/137] pixel: add Grayscale2bit color (#817) * pixel: add GrayScale2bit color * pixel: fix spelling of 'GrayScale' to 'Grayscale' --- pixel/image.go | 21 +++++++++++++++ pixel/image_test.go | 63 +++++++++++++++++++++++++++++++++++++++++++++ pixel/pixel.go | 32 ++++++++++++++++++++++- 3 files changed, 115 insertions(+), 1 deletion(-) diff --git a/pixel/image.go b/pixel/image.go index b3ef562..77b2f90 100644 --- a/pixel/image.go +++ b/pixel/image.go @@ -149,6 +149,20 @@ func (img Image[T]) setPixel(index int, c T) { } return + case zeroColor.BitsPerPixel() == 2: + // Grayscale2bit. + offset := index / 4 // 4 pixels per byte + shift := 6 - (index%4)*2 // bits: 6, 4, 2, 0 + + ptr := (*byte)(unsafe.Add(img.data, offset)) + + raw := *(*uint8)(unsafe.Pointer(&c)) + gray := raw & 0b11 + + mask := byte(0b11 << shift) + *ptr = (*ptr &^ mask) | (gray << shift) + return + case zeroColor.BitsPerPixel()%8 == 0: // Each color starts at a whole byte offset. // This is the easy case. @@ -206,6 +220,13 @@ func (img Image[T]) Get(x, y int) T { ptr := (*byte)(unsafe.Add(img.data, offset)) c = ((*ptr >> (7 - uint8(bits))) & 0x1) > 0 return any(c).(T) + case zeroColor.BitsPerPixel() == 2: + // Grayscale2bit. + offset := index / 4 // 4 pixels per byte + shift := 6 - (index%4)*2 // bits: 6, 4, 2, 0 + ptr := (*byte)(unsafe.Add(img.data, offset)) + value := ((*ptr) >> shift) & 0b11 + return any(Grayscale2bit(value)).(T) case zeroColor.BitsPerPixel()%8 == 0: // Colors like RGB565, RGB888, etc. offset := index * int(unsafe.Sizeof(zeroColor)) diff --git a/pixel/image_test.go b/pixel/image_test.go index 5636f3d..342f81b 100644 --- a/pixel/image_test.go +++ b/pixel/image_test.go @@ -107,6 +107,66 @@ func TestImageRGB444BE(t *testing.T) { } } +func TestImageGrayscale2bit(t *testing.T) { + image := pixel.NewImage[pixel.Grayscale2bit](128, 64) + + if width, height := image.Size(); width != 128 || height != 64 { + t.Errorf("image.Size(): expected 128, 64 but got %d, %d", width, height) + } + + // Define test colors representing 4 Grayscale levels. + testColors := []color.RGBA{ + {R: 0x00, G: 0x00, B: 0x00, A: 0xff}, // black + {R: 0x55, G: 0x55, B: 0x55, A: 0xff}, // dark gray + {R: 0xaa, G: 0xaa, B: 0xaa, A: 0xff}, // light gray + {R: 0xff, G: 0xff, B: 0xff, A: 0xff}, // white + } + + // Single pixel roundtrip test at a fixed coordinate. + for _, c := range testColors { + encoded := pixel.NewColor[pixel.Grayscale2bit](c.R, c.G, c.B) + image.Set(5, 3, encoded) + actual := image.Get(5, 3).RGBA() + if actual != c { + t.Errorf("failed to roundtrip color: expected %v but got %v", c, actual) + } + } + + // Multi-coordinate test across the image. + for x := 0; x < 8; x++ { + for y, c := range testColors { + encoded := pixel.NewColor[pixel.Grayscale2bit](c.R, c.G, c.B) + image.Set(x, y, encoded) + actual := image.Get(x, y).RGBA() + if actual != c { + t.Errorf("Set/Get mismatch at (%d,%d): expected %v but got %v", x, y, c, actual) + } + } + } +} + +func TestNewGrayscale2bitMapping(t *testing.T) { + testCases := []struct { + input color.RGBA + expect pixel.Grayscale2bit + }{ + {color.RGBA{R: 0x00, G: 0x00, B: 0x00}, 0}, // 0 + {color.RGBA{R: 0x3F, G: 0x3F, B: 0x3F}, 0}, // 63 + {color.RGBA{R: 0x40, G: 0x40, B: 0x40}, 1}, // 64 + {color.RGBA{R: 0x7F, G: 0x7F, B: 0x7F}, 1}, // 127 + {color.RGBA{R: 0x80, G: 0x80, B: 0x80}, 2}, // 128 + {color.RGBA{R: 0xBF, G: 0xBF, B: 0xBF}, 2}, // 191 + {color.RGBA{R: 0xC0, G: 0xC0, B: 0xC0}, 3}, // 192 + {color.RGBA{R: 0xFF, G: 0xFF, B: 0xFF}, 3}, // 255 + } + for _, tc := range testCases { + actual := pixel.NewColor[pixel.Grayscale2bit](tc.input.R, tc.input.G, tc.input.B) + if actual != tc.expect { + t.Errorf("NewGrayscale2bit(%#v) = %d, want %d", tc.input, actual, tc.expect) + } + } +} + func TestImageMonochrome(t *testing.T) { image := pixel.NewImage[pixel.Monochrome](128, 64) if width, height := image.Size(); width != 128 || height != 64 { @@ -236,6 +296,9 @@ func TestImageNoise(t *testing.T) { t.Run("RGB444BE", func(t *testing.T) { testImageNoiseN[pixel.RGB444BE](t) }) + t.Run("Grayscale2bit", func(t *testing.T) { + testImageNoiseN[pixel.Grayscale2bit](t) + }) t.Run("Monochrome", func(t *testing.T) { testImageNoiseN[pixel.Monochrome](t) }) diff --git a/pixel/pixel.go b/pixel/pixel.go index b4582a6..de069d3 100644 --- a/pixel/pixel.go +++ b/pixel/pixel.go @@ -16,7 +16,7 @@ import ( // particular display. Each pixel is at least 1 byte in size. // The color format is sRGB (or close to it) in all cases except for 1-bit. type Color interface { - RGB888 | RGB565BE | RGB555 | RGB444BE | Monochrome + RGB888 | RGB565BE | RGB555 | RGB444BE | Grayscale2bit | Monochrome BaseColor } @@ -50,6 +50,8 @@ func NewColor[T Color](r, g, b uint8) T { return any(NewRGB555(r, g, b)).(T) case RGB444BE: return any(NewRGB444BE(r, g, b)).(T) + case Grayscale2bit: + return any(NewGrayscale2bit(r, g, b)).(T) case Monochrome: return any(NewMonochrome(r, g, b)).(T) default: @@ -204,6 +206,34 @@ func (c RGB444BE) RGBA() color.RGBA { return color } +// Grayscale2bit represents a 2-bit Grayscale value (4 levels: black, dark gray, light gray, white). +type Grayscale2bit uint8 + +func NewGrayscale2bit(r, g, b uint8) Grayscale2bit { + // Convert RGB to luminance using standard weights (approximation of human perception) + // Use shift-based operations to reduce processing time. + // luminance := (299*uint32(r) + 587*uint32(g) + 114*uint32(b)) / 1000 + luminance := (77*uint32(r) + 150*uint32(g) + 29*uint32(b)) >> 8 + // Map to 2-bit value: 0–63 => 0, 64–127 => 1, 128–191 => 2, 192–255 => 3 + return Grayscale2bit((luminance >> 6) & 0b11) +} + +func (c Grayscale2bit) BitsPerPixel() int { + return 2 +} + +func (c Grayscale2bit) RGBA() color.RGBA { + // Expand 2-bit Grayscale back to 8-bit (0–255) using multiplication + // 0 → 0x00, 1 → 0x55, 2 → 0xAA, 3 → 0xFF (i.e., multiply by 85) + gray := uint8(c&0b11) * 85 + return color.RGBA{ + R: gray, + G: gray, + B: gray, + A: 255, + } +} + type Monochrome bool func NewMonochrome(r, g, b uint8) Monochrome { From d4654668f279517a38b31aca206947b712de0087 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 21 Nov 2025 13:54:12 +0100 Subject: [PATCH 102/137] scd4x: update package to use standard methods This deprecates the older Read* methods and uses standard Update() and sensor data getters instead. Apart from being more efficient, this also makes the driver more efficient when reading multiple sensors (since the SCD4x sensor won't get polled for new data at each Read* method call). --- scd4x/scd4x.go | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/scd4x/scd4x.go b/scd4x/scd4x.go index e605254..9b89e47 100644 --- a/scd4x/scd4x.go +++ b/scd4x/scd4x.go @@ -93,7 +93,18 @@ func (d *Device) ReadData() error { return nil } +// Update reads new data from the sensor (if new data is available) and caches +// it for reading in the CO2, Temperature, and Humidity methods. +func (d *Device) Update(measurements drivers.Measurement) error { + if measurements&(drivers.Temperature|drivers.Humidity|drivers.Concentration) != 0 { + return d.ReadData() + } + return nil +} + // ReadCO2 returns the CO2 concentration in PPM (parts per million). +// +// Deprecated: use Update() and CO2() instead. func (d *Device) ReadCO2() (co2 int32, err error) { ok, err := d.DataReady() if err != nil { @@ -105,7 +116,14 @@ func (d *Device) ReadCO2() (co2 int32, err error) { return int32(d.co2), err } +// CO2 returns last read the CO2 concentration in PPM (parts per million). +func (d *Device) CO2() int32 { + return int32(d.co2) +} + // ReadTemperature returns the temperature in celsius milli degrees (°C/1000) +// +// Deprecated: use Update() and Temperature() instead. func (d *Device) ReadTemperature() (temperature int32, err error) { ok, err := d.DataReady() if err != nil { @@ -114,8 +132,14 @@ func (d *Device) ReadTemperature() (temperature int32, err error) { if ok { err = d.ReadData() } + return d.Temperature(), err +} + +// Temperature returns the last read temperature in celsius milli degrees +// (°C/1000). +func (d *Device) Temperature() int32 { // temp = -45 + 175 * value / 2¹⁶ - return (-1 * 45000) + (21875 * (int32(d.temperature)) / 8192), err + return (-1 * 45000) + (21875 * (int32(d.temperature)) / 8192) } // ReadTempC returns the value in the temperature value in Celsius. @@ -130,6 +154,11 @@ func (d *Device) ReadTempF() float32 { } // ReadHumidity returns the current relative humidity in %rH. +// +// Warning: the value returned here is less precise than the humidity returned +// from Humidity()! +// +// Deprecated: use Update() and Temperature() instead. func (d *Device) ReadHumidity() (humidity int32, err error) { ok, err := d.DataReady() if err != nil { @@ -142,6 +171,15 @@ func (d *Device) ReadHumidity() (humidity int32, err error) { return (25 * int32(d.humidity)) / 16384, err } +// Humidity returns the relative humidity in hundredths of a percent (in other +// words, with a range 0..10_000). +// +// Warning: the value returned here is of a different scale (more precise) than +// ReadHumidity()! +func (d *Device) Humidity() int32 { + return (2500 * int32(d.humidity)) / 16384 +} + func (d *Device) sendCommand(command uint16) error { binary.BigEndian.PutUint16(d.tx[0:], command) return d.bus.Tx(uint16(d.Address), d.tx[0:2], nil) From fab688a701c2218cb6b10504c94dab7e7e939f96 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 21 Nov 2025 13:57:39 +0100 Subject: [PATCH 103/137] scd4x: remove dead code --- scd4x/scd4x.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/scd4x/scd4x.go b/scd4x/scd4x.go index 9b89e47..68c5e50 100644 --- a/scd4x/scd4x.go +++ b/scd4x/scd4x.go @@ -185,13 +185,6 @@ func (d *Device) sendCommand(command uint16) error { return d.bus.Tx(uint16(d.Address), d.tx[0:2], nil) } -func (d *Device) sendCommandWithValue(command, value uint16) error { - binary.BigEndian.PutUint16(d.tx[0:], command) - binary.BigEndian.PutUint16(d.tx[2:], value) - d.tx[4] = crc8(d.tx[2:4]) - return d.bus.Tx(uint16(d.Address), d.tx[0:5], nil) -} - func (d *Device) sendCommandWithResult(command uint16, result []byte) error { binary.BigEndian.PutUint16(d.tx[0:], command) if err := d.bus.Tx(uint16(d.Address), d.tx[0:2], nil); err != nil { From 9ed648f4a569d6d984212722cdb1d8025faa3868 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 28 Nov 2025 10:32:46 +0100 Subject: [PATCH 104/137] scd4x: add support for SCD41 single-shot measurements I have a SCD41, and would like to use it for single shot measurements instead of "low power periodic" measurements to achieve much lower average current consumption. --- scd4x/registers.go | 1 + scd4x/scd4x.go | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/scd4x/registers.go b/scd4x/registers.go index af057b4..fb25864 100644 --- a/scd4x/registers.go +++ b/scd4x/registers.go @@ -22,4 +22,5 @@ const ( CmdStartLowPowerPeriodicMeasurement = 0x21AC CmdStartPeriodicMeasurement = 0x21B1 CmdStopPeriodicMeasurement = 0x3F86 + CmdMeasureSingleShot = 0x219D ) diff --git a/scd4x/scd4x.go b/scd4x/scd4x.go index 68c5e50..a53bb9d 100644 --- a/scd4x/scd4x.go +++ b/scd4x/scd4x.go @@ -82,6 +82,13 @@ func (d *Device) StartLowPowerPeriodicMeasurement() error { return d.sendCommand(CmdStartLowPowerPeriodicMeasurement) } +// MeasureSingleShot starts a single measurement cycle (SCD41 only). After this +// command is complete, the caller should wait for 5000ms before trying to read +// the result. +func (d *Device) MeasureSingleShot() error { + return d.sendCommand(CmdMeasureSingleShot) +} + // ReadData reads the data from the sensor and caches it. func (d *Device) ReadData() error { if err := d.sendCommandWithResult(CmdReadMeasurement, d.rx[0:9]); err != nil { From 936a255df9f375477d969abd7a3731666f3b143e Mon Sep 17 00:00:00 2001 From: Mike Hughes Date: Thu, 11 Dec 2025 05:07:04 +1100 Subject: [PATCH 105/137] Add support for CEVA BNO08x 9DoF sensor (#809) * Add support for CEVA BNO08x 9DoF sensor. Also includes implementation of CEVA SH-2 and SHTP protocols. * Replace machine.I2C with drivers.I2C interface to remove dependency on machine package * Replace Pin functionality with that provided by tinygo.org/x/drivers/internal/pin * Unexport fields on SensorValue and replace with accessor methods. Add check for correct SensorID validation * Add example to smoketest.sh * Change build target for smoketest to match development environment.. Probably not important, but matches reality. * Fix decoding of some sensor data: Step Counter, Tap Detector, Flip Detector. These are experimental. * Refactor to allow SPI/UART etc. SPI is currently under development, but is omitted from this commit. Example code has been moved to i2c subdirectory. This commit introduces some major refactoring changes. It introduces a "Buser" interface and tries to remove any I2C specific code from the core. It still retains a couple of I2C specific fields in the "Config" struct ("Address" and "ReadChunk") but they are ignored in the as yet uncommited SPI code. * Fix CRLF -> LF for gofmt * Update smoketest to point to new example file --- bno08x/bno08x.go | 256 ++++++++++++++++ bno08x/bno08x_i2c.go | 173 +++++++++++ bno08x/constants.go | 179 +++++++++++ bno08x/decode.go | 316 ++++++++++++++++++++ bno08x/hal.go | 43 +++ bno08x/sh2.go | 387 ++++++++++++++++++++++++ bno08x/shtp.go | 83 ++++++ bno08x/types.go | 572 ++++++++++++++++++++++++++++++++++++ examples/bno08x/i2c/main.go | 66 +++++ smoketest.sh | 1 + 10 files changed, 2076 insertions(+) create mode 100644 bno08x/bno08x.go create mode 100644 bno08x/bno08x_i2c.go create mode 100644 bno08x/constants.go create mode 100644 bno08x/decode.go create mode 100644 bno08x/hal.go create mode 100644 bno08x/sh2.go create mode 100644 bno08x/shtp.go create mode 100644 bno08x/types.go create mode 100644 examples/bno08x/i2c/main.go diff --git a/bno08x/bno08x.go b/bno08x/bno08x.go new file mode 100644 index 0000000..95cc3df --- /dev/null +++ b/bno08x/bno08x.go @@ -0,0 +1,256 @@ +// Package bno08x provides a TinyGo driver for the Adafruit BNO08x 9-DOF IMU sensors. +// +// This driver implements the CEVA SH-2 protocol over the SHTP transport layer, +// providing access to orientation, motion, and environmental sensors. +// +// Datasheet: https://www.ceva-ip.com/wp-content/uploads/BNO080_085-Datasheet.pdf +package bno08x + +import ( + "time" + + "tinygo.org/x/drivers/internal/pin" +) + +// Buser is the interface that wraps I2C or SPI bus operations. +type Buser interface { + configure(address uint16, readChunk int) error + read(target []byte) (int, uint32, error) + write(data []byte) error + softReset() error +} + +// Device represents a BNO08x sensor device. +type Device struct { + bus Buser + resetPin pin.OutputFunc + + hal *hal + shtp *shtp + sh2 *sh2Protocol + + queue [8]SensorValue + queueHead int + queueTail int + queueCount int + + productIDs ProductIDs + lastReset bool +} + +// Config holds configuration options for the device. +type Config struct { + // Address is the I2C address (used only for I2C bus). + Address uint16 + + // ResetPin is the optional hardware reset pin. + ResetPin pin.OutputFunc + + // ReadChunk is the I2C read chunk size (used only for I2C bus). + ReadChunk int + + // StartupDelay is the delay after reset (default: 100ms). + StartupDelay time.Duration +} + +// Configure initializes the sensor and prepares it for use. +func (d *Device) Configure(cfg Config) error { + // Configure bus-specific settings + if err := d.bus.configure(cfg.Address, cfg.ReadChunk); err != nil { + return err + } + + if cfg.ResetPin != nil { + d.resetPin = cfg.ResetPin + } + if cfg.StartupDelay <= 0 { + cfg.StartupDelay = 100 * time.Millisecond + } + + d.hal = newHAL(d) + d.shtp = newSHTP(d.hal) + d.sh2 = newSH2Protocol(d) + + d.queueHead = 0 + d.queueTail = 0 + d.queueCount = 0 + d.productIDs = ProductIDs{} + d.lastReset = false + + if err := d.hal.open(); err != nil { + return err + } + + // Now that handlers are registered, perform reset + // Try hardware reset first if available + if d.resetPin != nil { + d.hardwareReset() + time.Sleep(cfg.StartupDelay) + } else { + // No hardware reset pin - try soft reset via bus + if err := d.bus.softReset(); err != nil { + // If that fails, try soft reset via SHTP protocol + _ = d.sh2.softReset() + time.Sleep(50 * time.Millisecond) + } + } + + // Wait for reset notification by actively polling + // The sensor should send reset complete message shortly after reset + deadline := time.Now().Add(1000 * time.Millisecond) + pollCount := 0 + for time.Now().Before(deadline) { + pollCount++ + if err := d.service(); err != nil { + // Ignore errors during initial polling - sensor might not be ready + time.Sleep(1 * time.Millisecond) + continue + } + if d.lastReset { + break + } + time.Sleep(1 * time.Millisecond) + } + + if !d.lastReset { + return errTimeout + } + + // NOTE: We intentionally skip the Initialize command (sh2_initialize) + // Testing revealed that sending the Initialize command (0xF2 0x00 0x04 0x01...) + // prevents the BNO08x from sending sensor reports on channel 3. + // The sensor works correctly without this command after a soft reset. + // The Arduino library likely works because it does a hardware reset which + // may put the sensor in a different state, or their initialization sequence + // differs in a way that doesn't trigger this issue. + + // Request product IDs + if err := d.sh2.requestProductIDs(); err != nil { + return err + } + + // Wait for product IDs with polling delay + deadline = time.Now().Add(500 * time.Millisecond) + for time.Now().Before(deadline) { + if err := d.service(); err != nil { + time.Sleep(10 * time.Millisecond) + continue + } + if d.productIDs.NumEntries > 0 { + break + } + time.Sleep(10 * time.Millisecond) + } + + if d.productIDs.NumEntries == 0 { + return errTimeout + } + + return nil +} + +// EnableReport enables a specific sensor report at the given interval. +func (d *Device) EnableReport(id SensorID, intervalUs uint32) error { + err := d.sh2.enableReport(id, intervalUs) + if err != nil { + return err + } + + // Poll a few times to let the sensor process the command + // and potentially send acknowledgment + for i := 0; i < 10; i++ { + _ = d.service() + time.Sleep(10 * time.Millisecond) + } + + return nil +} + +// GetSensorConfig retrieves the current configuration for a sensor. +func (d *Device) GetSensorConfig(id SensorID) (SensorConfig, error) { + return d.sh2.getSensorConfig(id) +} + +// SetSensorConfig sets the configuration for a sensor. +func (d *Device) SetSensorConfig(id SensorID, config SensorConfig) error { + return d.sh2.setSensorConfig(id, config) +} + +// WasReset returns true if the sensor signaled a reset since the last call. +func (d *Device) WasReset() bool { + if d.lastReset { + d.lastReset = false + return true + } + return false +} + +// GetSensorEvent retrieves the next available sensor event if present. +func (d *Device) GetSensorEvent() (SensorValue, bool) { + if d.queueCount == 0 { + if err := d.service(); err != nil { + return SensorValue{}, false + } + if d.queueCount == 0 { + return SensorValue{}, false + } + } + + value := d.queue[d.queueHead] + d.queueHead = (d.queueHead + 1) % len(d.queue) + d.queueCount-- + + return value, true +} + +// ProductIDs returns the cached product identification information. +func (d *Device) ProductIDs() ProductIDs { + return d.productIDs +} + +// Service processes pending sensor data. +// This is called automatically by GetSensorEvent but can be called manually +// for more control over timing. +func (d *Device) Service() error { + return d.service() +} + +func (d *Device) enqueue(value SensorValue) { + next := (d.queueTail + 1) % len(d.queue) + if d.queueCount == len(d.queue) { + // Queue full, drop oldest + d.queueHead = (d.queueHead + 1) % len(d.queue) + d.queueCount-- + } + d.queue[d.queueTail] = value + d.queueTail = next + d.queueCount++ +} + +func (d *Device) service() error { + if d.shtp == nil { + return nil + } + for { + processed, err := d.shtp.poll() + if err != nil { + return err + } + if !processed { + break + } + } + return nil +} + +func (d *Device) hardwareReset() { + if d.resetPin == nil { + return + } + d.resetPin.High() + time.Sleep(10 * time.Millisecond) + d.resetPin.Low() + time.Sleep(10 * time.Millisecond) + d.resetPin.High() + time.Sleep(10 * time.Millisecond) +} diff --git a/bno08x/bno08x_i2c.go b/bno08x/bno08x_i2c.go new file mode 100644 index 0000000..7546025 --- /dev/null +++ b/bno08x/bno08x_i2c.go @@ -0,0 +1,173 @@ +package bno08x + +import ( + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/pin" +) + +// I2CConfig holds I2C-specific configuration options. +type I2CConfig struct { + // Address is the I2C address (default: 0x4A). + Address uint16 + + // ResetPin is the optional hardware reset pin. + ResetPin pin.OutputFunc + + // ReadChunk is the I2C read chunk size (default: 32 bytes). + ReadChunk int +} + +const ( + // DefaultAddress is the default I2C address. + DefaultAddress = 0x4A +) + +// NewI2C creates a new BNO08x device using I2C communication. +func NewI2C(bus drivers.I2C) *Device { + return &Device{ + bus: &I2CBus{ + wire: bus, + address: DefaultAddress, + readChunk: i2cDefaultChunk, + }, + } +} + +// I2CBus implements the Buser interface for I2C communication. +type I2CBus struct { + wire drivers.I2C + address uint16 + readChunk int + scratch []byte + header [shtpHeaderLength]byte +} + +// configure sets up the I2C bus with the specified address and chunk size. +func (b *I2CBus) configure(address uint16, readChunk int) error { + if address != 0 { + b.address = address + } + if readChunk > 0 { + b.readChunk = readChunk + } + + chunk := b.readChunk + if chunk < shtpHeaderLength { + chunk = shtpHeaderLength + } + b.scratch = make([]byte, chunk) + + return nil +} + +// read reads data from the I2C bus. +func (b *I2CBus) read(target []byte) (int, uint32, error) { + // Read SHTP header (4 bytes) to get packet length + // Use pre-allocated header buffer to avoid allocations + err := b.wire.Tx(b.address, nil, b.header[:]) + if err != nil { + return 0, 0, err + } + + // Parse packet length from header + packetLen := uint16(b.header[0]) | (uint16(b.header[1]) << 8) + + // Check if continuation bit is set (0x8000) + // This means no data is available yet + if packetLen&continueMask != 0 { + return 0, 0, nil + } + + // No continuation bit, check for actual data + if packetLen == 0 { + return 0, 0, nil + } + + if int(packetLen) > len(target) { + return 0, 0, errBufferTooSmall + } + + // Now read the full packet in chunks, re-reading the header in first chunk + // This follows Arduino's approach: initial header read is just to get size, + // actual packet data (including header) is read in the loop + cargoRemaining := int(packetLen) + offset := 0 + firstRead := true + + for cargoRemaining > 0 { + var request int + if firstRead { + // First read: get the full packet including header (up to chunkSize) + request = b.readChunk + if request > cargoRemaining { + request = cargoRemaining + } + } else { + // Subsequent reads: each chunk has a 4-byte header we need to skip + request = b.readChunk + if request > cargoRemaining+shtpHeaderLength { + request = cargoRemaining + shtpHeaderLength + } + } + + // Ensure scratch buffer is large enough + if request > len(b.scratch) { + b.scratch = make([]byte, request) + } + buf := b.scratch[:request] + + // Read chunk + err = b.wire.Tx(b.address, nil, buf) + if err != nil { + return 0, 0, err + } + + var cargoRead int + if firstRead { + // First read: copy everything including header + cargoRead = request + copy(target[offset:], buf[:cargoRead]) + firstRead = false + } else { + // Subsequent reads: skip the 4-byte header + cargoRead = request - shtpHeaderLength + copy(target[offset:], buf[shtpHeaderLength:shtpHeaderLength+cargoRead]) + } + + offset += cargoRead + cargoRemaining -= cargoRead + } + + // Extract timestamp from the header in the target buffer + timestamp := uint32(target[2]) | (uint32(target[3]) << 8) + + return int(packetLen), timestamp, nil +} + +// write sends data over the I2C bus. +func (b *I2CBus) write(data []byte) error { + return b.wire.Tx(b.address, data, nil) +} + +// softReset sends a soft reset command via I2C. +func (b *I2CBus) softReset() error { + // Send soft reset packet via I2C as per Adafruit implementation + // Format: [length_low, length_high, channel, sequence, command] + // This is: 5 bytes total, channel 1 (executable), command 1 (reset) + softResetPacket := []byte{5, 0, 1, 0, 1} + + // Try up to 5 times + var err error + for i := 0; i < 5; i++ { + err = b.wire.Tx(b.address, softResetPacket, nil) + if err == nil { + // Success - wait for sensor to process reset + time.Sleep(300 * time.Millisecond) + return nil + } + time.Sleep(30 * time.Millisecond) + } + return err +} diff --git a/bno08x/constants.go b/bno08x/constants.go new file mode 100644 index 0000000..7073f1f --- /dev/null +++ b/bno08x/constants.go @@ -0,0 +1,179 @@ +package bno08x + +// I2C and protocol constants +const ( + shtpHeaderLength = 4 + maxTransferOut = 256 + maxTransferIn = 384 + + i2cDefaultChunk = 32 + continueMask = 0x8000 +) + +// SHTP channel numbers +const ( + channelCommand = 0 + channelExecutable = 1 + channelControl = 2 + channelSensorReport = 3 + channelWakeReport = 4 + channelGyroRV = 5 +) + +// SH-2 report IDs +const ( + reportProdIDReq = 0xF9 + reportProdIDResp = 0xF8 + reportSetFeature = 0xFD + reportGetFeature = 0xFE + reportGetFeatureResp = 0xFC + reportCommandReq = 0xF2 + reportCommandResp = 0xF1 + reportFRSWriteReq = 0xF7 + reportFRSWriteData = 0xF6 + reportFRSReadReq = 0xF4 + reportFRSReadResp = 0xF3 + reportBaseTimestamp = 0xFB + reportTimestampReuse = 0xFA + reportForceFlush = 0xF0 + reportFlushCompleted = 0xEF + reportResetReq = 0xF1 + reportResetResp = 0xF0 +) + +// SH-2 commands +const ( + cmdErrors = 0x01 + cmdCounts = 0x02 + cmdTare = 0x03 + cmdInitialize = 0x04 + cmdFRS = 0x05 + cmdDCD = 0x06 + cmdMECal = 0x07 + cmdProdIDReq = 0x07 + cmdDCDSave = 0x09 + cmdGetOscType = 0x0A + cmdClearDCDReset = 0x0B + cmdCal = 0x0C + cmdBootloader = 0x0D + cmdInteractiveZRO = 0x0E + + // Command parameters + initSystem = 0x01 + initUnsolicited = 0x80 + + countsClearCounts = 0x01 + countsGetCounts = 0x00 + + tareTareNow = 0x00 + tarePersist = 0x01 + tareSetReorientation = 0x02 + + calStart = 0x00 + calFinish = 0x01 + + commandParamCount = 9 + responseValueCount = 11 +) + +// Feature report flags +const ( + featChangeSensitivityRelative = 0x01 + featChangeSensitivityEnabled = 0x02 + featWakeEnabled = 0x04 + featAlwaysOnEnabled = 0x08 +) + +// Scaling factors for sensor data +// These are derived from the Q-point encoding in the SH-2 specification +const ( + scaleQuat = 1.0 / 16384.0 // Q14 + scaleAccel = 1.0 / 256.0 // Q8 + scaleGyro = 1.0 / 512.0 // Q9 + scaleMag = 1.0 / 16.0 // Q4 + scaleAccuracy = 1.0 / 4096.0 // Q12 + scalePressure = 1.0 / 1048576.0 // Q20 + scaleLight = 1.0 / 256.0 // Q8 + scaleHumidity = 1.0 / 256.0 // Q8 + scaleProximity = 1.0 / 16.0 // Q4 + scaleTemperature = 1.0 / 128.0 // Q7 + scaleAngle = 1.0 / 16.0 // Q4 + scaleHeartRate = 1.0 / 16.0 // Q4 +) + +// Activity classifier codes (extended beyond standard SH-2) +const ( + ActivityUnknown = 0 + ActivityInVehicle = 1 + ActivityOnBicycle = 2 + ActivityOnFoot = 3 + ActivityStill = 4 + ActivityTilting = 5 + ActivityWalking = 6 + ActivityRunning = 7 + ActivityOnStairs = 8 + ActivityOptionCount = 9 +) + +// Stability classifier values +const ( + StabilityUnknown = 0 + StabilityOnTable = 1 + StabilityStationary = 2 + StabilityStable = 3 + StabilityMotion = 4 +) + +// Tap detector flags +const ( + TapX = 0x01 // 1 - X axis tapped + TapXPos = 0x02 // 2 - X positive direction + TapY = 0x04 // 4 - Y axis tapped + TapYPos = 0x08 // 8 - Y positive direction + TapZ = 0x10 // 16 - Z axis tapped + TapZPos = 0x20 // 32 - Z positive direction + TapDouble = 0x40 // 64 - Double tap occurred +) + +// GUID values for SHTP +const ( + guidSHTP = 0 + guidExecutable = 1 + guidSensorHub = 2 +) + +// Advertisement tags +const ( + tagNull = 0 + tagGUID = 1 + tagMaxCargoHeaderWrite = 2 + tagMaxCargoHeaderRead = 3 + tagMaxTransferWrite = 4 + tagMaxTransferRead = 5 + tagNormalChannel = 6 + tagWakeChannel = 7 + tagAppName = 8 + tagChannelName = 9 + tagAdvCount = 10 + tagAppSpecific = 0x80 + tagSH2Version = 0x80 + tagSH2ReportLengths = 0x81 +) + +// Timeouts +const ( + advertTimeout = 200000 // microseconds + commandTimeout = 300000 // microseconds +) + +// Executable device commands +const ( + execDeviceCmdReset = 1 + execDeviceCmdOn = 2 + execDeviceCmdSleep = 3 +) + +// Executable device responses +const ( + execDeviceRespResetComplete = 1 +) diff --git a/bno08x/decode.go b/bno08x/decode.go new file mode 100644 index 0000000..0ec2a15 --- /dev/null +++ b/bno08x/decode.go @@ -0,0 +1,316 @@ +package bno08x + +import "encoding/binary" + +// decodeSensor decodes a sensor report payload into a SensorValue. +func decodeSensor(payload []byte, timestamp uint32) (SensorValue, bool) { + if len(payload) < 4 { + return SensorValue{}, false + } + + value := SensorValue{ + id: SensorID(payload[0]), + sequence: payload[1], + status: payload[2] & 0x03, + delay: payload[3], + timestamp: uint64(timestamp), + } + + data := payload[4:] + + switch value.id { + case SensorRawAccelerometer: + if len(data) >= 10 { + value.rawAccelerometer = RawVector3{ + X: int16(binary.LittleEndian.Uint16(data[0:])), + Y: int16(binary.LittleEndian.Uint16(data[2:])), + Z: int16(binary.LittleEndian.Uint16(data[4:])), + Timestamp: binary.LittleEndian.Uint32(data[6:]), + } + } + + case SensorAccelerometer: + if len(data) >= 6 { + value.accelerometer = Vector3{ + X: qToFloat(data[0:], scaleAccel), + Y: qToFloat(data[2:], scaleAccel), + Z: qToFloat(data[4:], scaleAccel), + } + } + + case SensorLinearAcceleration: + if len(data) >= 6 { + value.linearAcceleration = Vector3{ + X: qToFloat(data[0:], scaleAccel), + Y: qToFloat(data[2:], scaleAccel), + Z: qToFloat(data[4:], scaleAccel), + } + } + + case SensorGravity: + if len(data) >= 6 { + value.gravity = Vector3{ + X: qToFloat(data[0:], scaleAccel), + Y: qToFloat(data[2:], scaleAccel), + Z: qToFloat(data[4:], scaleAccel), + } + } + + case SensorRawGyroscope: + if len(data) >= 12 { + value.rawGyroscope = RawGyroscope{ + X: int16(binary.LittleEndian.Uint16(data[0:])), + Y: int16(binary.LittleEndian.Uint16(data[2:])), + Z: int16(binary.LittleEndian.Uint16(data[4:])), + Temperature: int16(binary.LittleEndian.Uint16(data[6:])), + Timestamp: binary.LittleEndian.Uint32(data[8:]), + } + } + + case SensorGyroscope: + if len(data) >= 6 { + value.gyroscope = Vector3{ + X: qToFloat(data[0:], scaleGyro), + Y: qToFloat(data[2:], scaleGyro), + Z: qToFloat(data[4:], scaleGyro), + } + } + + case SensorGyroscopeUncalibrated: + if len(data) >= 12 { + value.gyroscopeUncal = GyroscopeUncalibrated{ + X: qToFloat(data[0:], scaleGyro), + Y: qToFloat(data[2:], scaleGyro), + Z: qToFloat(data[4:], scaleGyro), + BiasX: qToFloat(data[6:], scaleGyro), + BiasY: qToFloat(data[8:], scaleGyro), + BiasZ: qToFloat(data[10:], scaleGyro), + } + } + + case SensorRawMagnetometer: + if len(data) >= 10 { + value.rawMagnetometer = RawVector3{ + X: int16(binary.LittleEndian.Uint16(data[0:])), + Y: int16(binary.LittleEndian.Uint16(data[2:])), + Z: int16(binary.LittleEndian.Uint16(data[4:])), + Timestamp: binary.LittleEndian.Uint32(data[6:]), + } + } + + case SensorMagneticField: + if len(data) >= 6 { + value.magneticField = Vector3{ + X: qToFloat(data[0:], scaleMag), + Y: qToFloat(data[2:], scaleMag), + Z: qToFloat(data[4:], scaleMag), + } + } + + case SensorMagneticFieldUncalibrated: + if len(data) >= 12 { + value.magneticFieldUncal = MagneticFieldUncalibrated{ + X: qToFloat(data[0:], scaleMag), + Y: qToFloat(data[2:], scaleMag), + Z: qToFloat(data[4:], scaleMag), + BiasX: qToFloat(data[6:], scaleMag), + BiasY: qToFloat(data[8:], scaleMag), + BiasZ: qToFloat(data[10:], scaleMag), + } + } + + case SensorRotationVector: + if len(data) >= 10 { + value.quaternion = Quaternion{ + I: qToFloat(data[0:], scaleQuat), + J: qToFloat(data[2:], scaleQuat), + K: qToFloat(data[4:], scaleQuat), + Real: qToFloat(data[6:], scaleQuat), + } + value.quaternionAccuracy = qToFloat(data[8:], scaleAccuracy) + } + + case SensorGameRotationVector: + if len(data) >= 8 { + value.quaternion = Quaternion{ + I: qToFloat(data[0:], scaleQuat), + J: qToFloat(data[2:], scaleQuat), + K: qToFloat(data[4:], scaleQuat), + Real: qToFloat(data[6:], scaleQuat), + } + } + + case SensorGeomagneticRotationVector: + if len(data) >= 10 { + value.quaternion = Quaternion{ + I: qToFloat(data[0:], scaleQuat), + J: qToFloat(data[2:], scaleQuat), + K: qToFloat(data[4:], scaleQuat), + Real: qToFloat(data[6:], scaleQuat), + } + value.quaternionAccuracy = qToFloat(data[8:], scaleAccuracy) + } + + case SensorARVRStabilizedRV: + if len(data) >= 10 { + value.quaternion = Quaternion{ + I: qToFloat(data[0:], scaleQuat), + J: qToFloat(data[2:], scaleQuat), + K: qToFloat(data[4:], scaleQuat), + Real: qToFloat(data[6:], scaleQuat), + } + value.quaternionAccuracy = qToFloat(data[8:], scaleAccuracy) + } + + case SensorARVRStabilizedGRV: + if len(data) >= 8 { + value.quaternion = Quaternion{ + I: qToFloat(data[0:], scaleQuat), + J: qToFloat(data[2:], scaleQuat), + K: qToFloat(data[4:], scaleQuat), + Real: qToFloat(data[6:], scaleQuat), + } + } + + case SensorGyroIntegratedRV: + if len(data) >= 10 { + value.quaternion = Quaternion{ + I: qToFloat(data[0:], scaleQuat), + J: qToFloat(data[2:], scaleQuat), + K: qToFloat(data[4:], scaleQuat), + Real: qToFloat(data[6:], scaleQuat), + } + // Angular velocity X at data[8:10] + } + + case SensorPressure: + if len(data) >= 4 { + value.pressure = float32(int32(binary.LittleEndian.Uint32(data[0:]))) * scalePressure + } + + case SensorAmbientLight: + if len(data) >= 4 { + value.ambientLight = float32(int32(binary.LittleEndian.Uint32(data[0:]))) * scaleLight + } + + case SensorHumidity: + if len(data) >= 2 { + value.humidity = qToFloat(data[0:], scaleHumidity) + } + + case SensorProximity: + if len(data) >= 2 { + value.proximity = qToFloat(data[0:], scaleProximity) + } + + case SensorTemperature: + if len(data) >= 2 { + value.temperature = qToFloat(data[0:], scaleTemperature) + } + + case SensorTapDetector: + if len(data) >= 1 { + value.tapDetector = TapDetector{ + Flags: data[0], + } + } + + case SensorStepDetector: + if len(data) >= 4 { + value.stepDetector = StepDetector{ + Latency: binary.LittleEndian.Uint32(data[0:]), + } + } + + case SensorStepCounter: + if len(data) >= 8 { + value.stepCounter = StepCounter{ + Count: uint16(binary.LittleEndian.Uint32(data[4:8])), + Latency: binary.LittleEndian.Uint32(data[0:4]), + } + } + + case SensorSignificantMotion: + if len(data) >= 2 { + value.significantMotion = SignificantMotion{ + Motion: binary.LittleEndian.Uint16(data[0:]), + } + } + + case SensorStabilityClassifier: + if len(data) >= 1 { + value.stabilityClassifier = StabilityClassifier{ + Classification: data[0], + } + } + + case SensorStabilityDetector: + if len(data) >= 1 { + value.stabilityDetector = data[0] + } + + case SensorShakeDetector: + if len(data) >= 2 { + value.shakeDetector = ShakeDetector{ + Shake: binary.LittleEndian.Uint16(data[0:]), + } + } + + case SensorFlipDetector: + if len(data) >= 2 { + value.flipDetector = binary.LittleEndian.Uint16(data[0:2]) + } + + case SensorPickupDetector: + if len(data) >= 2 { + // Pickup detected at data[0:2] + } + + case SensorPersonalActivityClassifier: + if len(data) >= 16 { + value.personalActivityClassifier = PersonalActivityClassifier{ + Page: data[0], + MostLikelyState: data[1], + EndOfPage: data[15], + } + for i := 0; i < 10 && i+2 < len(data); i++ { + value.personalActivityClassifier.Confidence[i] = data[2+i] + } + } + + case SensorSleepDetector: + if len(data) >= 1 { + value.sleepDetector = data[0] + } + + case SensorTiltDetector: + if len(data) >= 1 { + value.tiltDetector = data[0] + } + + case SensorPocketDetector: + if len(data) >= 1 { + value.pocketDetector = data[0] + } + + case SensorCircleDetector: + if len(data) >= 1 { + value.circleDetector = data[0] + } + + case SensorHeartRateMonitor: + if len(data) >= 2 { + value.heartRateMonitor = binary.LittleEndian.Uint16(data[0:]) + } + } + + return value, true +} + +// qToFloat converts a Q-point fixed-point value to float32. +func qToFloat(data []byte, scale float32) float32 { + if len(data) < 2 { + return 0 + } + return float32(int16(binary.LittleEndian.Uint16(data))) * scale +} diff --git a/bno08x/hal.go b/bno08x/hal.go new file mode 100644 index 0000000..0e939d2 --- /dev/null +++ b/bno08x/hal.go @@ -0,0 +1,43 @@ +package bno08x + +import ( + "time" +) + +// hal implements the hardware abstraction layer for bus communication. +type hal struct { + device *Device +} + +func newHAL(dev *Device) *hal { + return &hal{ + device: dev, + } +} + +func (h *hal) open() error { + // HAL is now open and ready for communication + // Soft reset will be sent after handlers are registered + return nil +} + +func (h *hal) close() {} + +func (h *hal) read(target []byte) (int, uint32, error) { + return h.device.bus.read(target) +} + +func (h *hal) write(frame []byte) (int, error) { + if len(frame) > maxTransferOut { + return 0, errFrameTooLarge + } + err := h.device.bus.write(frame) + if err != nil { + return 0, err + } + return len(frame), nil +} + +func (h *hal) getTimeUs() uint32 { + return uint32(time.Now().UnixNano() / 1000) +} diff --git a/bno08x/sh2.go b/bno08x/sh2.go new file mode 100644 index 0000000..d756107 --- /dev/null +++ b/bno08x/sh2.go @@ -0,0 +1,387 @@ +// SH-2 specification found at https://www.ceva-ip.com/wp-content/uploads/SH-2-Reference-Manual.pdf + +package bno08x + +import ( + "encoding/binary" + "time" +) + +// getReportLen returns the length in bytes of a sensor report given its ID. +// Returns 0 for unknown report IDs. +func getReportLen(reportID byte) int { + switch reportID { + case 0xF1: // FLUSH_COMPLETED + return 6 + case 0xFA: // TIMESTAMP_REBASE + return 5 + case 0xFB: // BASE_TIMESTAMP_REF + return 5 + case 0xFC: // GET_FEATURE_RESP + return 17 + case 0x01: // Accelerometer (calibrated) + return 10 + case 0x02: // Gyroscope (calibrated) + return 10 + case 0x03: // Magnetic field (calibrated) + return 10 + case 0x04: // Linear acceleration + return 10 + case 0x05: // Rotation vector + return 14 + case 0x06: // Gravity + return 10 + case 0x07: // Gyroscope uncalibrated + return 16 + case 0x08: // Game rotation vector + return 12 + case 0x09: // Geomagnetic rotation vector + return 14 + case 0x0A: // Pressure + return 10 + case 0x0B: // Ambient light + return 10 + case 0x0C: // Humidity + return 10 + case 0x0D: // Proximity + return 10 + case 0x0E: // Temperature + return 10 + case 0x0F: // Magnetic field uncalibrated + return 16 + case 0x10: // Tap detector + return 5 + case 0x11: // Step counter + return 12 + case 0x12: // Significant motion + return 6 + case 0x13: // Stability classifier + return 5 + case 0x14: // Raw accelerometer + return 16 + case 0x15: // Raw gyroscope + return 16 + case 0x16: // Raw magnetometer + return 16 + case 0x18: // Step detector + return 8 + case 0x19: // Shake detector + return 6 + case 0x1A: // Flip detector + return 6 + case 0x1B: // Pickup detector + return 6 + case 0x1C: // Stability detector + return 6 + case 0x1E: // Personal activity classifier + return 16 + default: + // For most sensor reports, they are typically 10-16 bytes + // If we don't know the exact length, return a safe default + // that covers most cases (the handler will bounds-check) + if reportID < 0xF0 { + return 10 // Most sensor reports are at least this long + } + return 0 + } +} + +// sh2Protocol implements the Sensor Hub 2 (SH-2) application protocol. +type sh2Protocol struct { + device *Device + transport *shtp + cmdSeq uint8 + waiting bool + lastCmd uint8 + pendingConfigRequest bool + pendingConfigSensor SensorID + receivedConfig SensorConfig + configReady bool + configBuf [17]byte // Reusable buffer for setSensorConfig + commandBuf [3 + commandParamCount]byte // Reusable buffer for sendCommand +} + +func newSH2Protocol(device *Device) *sh2Protocol { + proto := &sh2Protocol{ + device: device, + transport: device.shtp, + } + + // Register handlers for each channel + device.shtp.register(channelControl, proto.handleControl) + device.shtp.register(channelSensorReport, proto.handleSensor) + device.shtp.register(channelWakeReport, proto.handleSensor) + device.shtp.register(channelGyroRV, proto.handleSensor) + device.shtp.register(channelExecutable, proto.handleExecutable) + + return proto +} + +// softReset sends a software reset command to the sensor. +func (s *sh2Protocol) softReset() error { + payload := []byte{execDeviceCmdReset} + return s.transport.send(channelExecutable, payload) +} + +// initialize sends the initialize command to the sensor. +func (s *sh2Protocol) initialize() error { + return s.sendCommand(cmdInitialize, []byte{initSystem}) +} + +// requestProductIDs requests product identification information. +func (s *sh2Protocol) requestProductIDs() error { + payload := []byte{reportProdIDReq, 0x00} + return s.transport.send(channelControl, payload) +} + +// enableReport enables a sensor report at the specified interval. +func (s *sh2Protocol) enableReport(id SensorID, intervalUs uint32) error { + config := SensorConfig{ + ReportInterval: intervalUs, + } + return s.setSensorConfig(id, config) +} + +// getSensorConfig retrieves the configuration for a sensor. +// This method sends a GET_FEATURE request and waits for the response +// by polling the device. It will timeout after approximately 1 second. +func (s *sh2Protocol) getSensorConfig(id SensorID) (SensorConfig, error) { + // Mark that we're waiting for a config response + s.pendingConfigRequest = true + s.pendingConfigSensor = id + s.configReady = false + + payload := []byte{reportGetFeature, byte(id)} + err := s.transport.send(channelControl, payload) + if err != nil { + s.pendingConfigRequest = false + return SensorConfig{}, err + } + + // Poll for response with timeout + maxAttempts := 100 // ~1 second with 10ms delays + for i := 0; i < maxAttempts; i++ { + // Service the device to process incoming messages + s.device.shtp.poll() + + if s.configReady { + s.pendingConfigRequest = false + s.configReady = false + return s.receivedConfig, nil + } + + // Small delay between polls + time.Sleep(10 * time.Millisecond) + } + + s.pendingConfigRequest = false + return SensorConfig{}, errTimeout +} + +// setSensorConfig configures a sensor. +func (s *sh2Protocol) setSensorConfig(id SensorID, config SensorConfig) error { + // Use pre-allocated buffer to avoid allocations + payload := s.configBuf[:] + payload[0] = reportSetFeature + payload[1] = byte(id) + + // Build feature flags + var flags uint8 + if config.ChangeSensitivityEnabled { + flags |= featChangeSensitivityEnabled + } + if config.ChangeSensitivityRelative { + flags |= featChangeSensitivityRelative + } + if config.WakeupEnabled { + flags |= featWakeEnabled + } + if config.AlwaysOnEnabled { + flags |= featAlwaysOnEnabled + } + payload[2] = flags + + binary.LittleEndian.PutUint16(payload[3:5], config.ChangeSensitivity) + binary.LittleEndian.PutUint32(payload[5:9], config.ReportInterval) + binary.LittleEndian.PutUint32(payload[9:13], config.BatchInterval) + binary.LittleEndian.PutUint32(payload[13:17], config.SensorSpecific) + + return s.transport.send(channelControl, payload) +} + +// sendCommand sends a command with parameters to the sensor. +func (s *sh2Protocol) sendCommand(command byte, params []byte) error { + // Use pre-allocated buffer to avoid allocations + payload := s.commandBuf[:] + payload[0] = reportCommandReq + payload[1] = s.cmdSeq + payload[2] = command + s.cmdSeq++ + s.lastCmd = command + s.waiting = true + + for i := 0; i < commandParamCount && i < len(params); i++ { + payload[3+i] = params[i] + } + + return s.transport.send(channelControl, payload[:3+commandParamCount]) +} + +// handleControl processes control channel messages. +func (s *sh2Protocol) handleControl(payload []byte, timestamp uint32) { + if len(payload) == 0 { + return + } + + reportID := payload[0] + + switch reportID { + case reportProdIDResp: + s.handleProdID(payload, timestamp) + case reportCommandResp: + s.handleCommandResp(payload, timestamp) + case reportGetFeatureResp: + s.handleGetFeatureResp(payload, timestamp) + case reportFRSReadResp: + // FRS (Flash Record System) read response + // Not implemented in basic version + } +} + +// handleProdID processes product ID responses. +func (s *sh2Protocol) handleProdID(payload []byte, timestamp uint32) { + if len(payload) < 16 { + return + } + + entry := ProductID{ + ResetCause: payload[1], + VersionMajor: payload[2], + VersionMinor: payload[3], + PartNumber: binary.LittleEndian.Uint32(payload[4:8]), + BuildNumber: binary.LittleEndian.Uint32(payload[8:12]), + VersionPatch: binary.LittleEndian.Uint16(payload[12:14]), + Reserved0: payload[14], + Reserved1: payload[15], + } + + // Store in first slot + s.device.productIDs.Entries[0] = entry + s.device.productIDs.NumEntries = 1 +} + +// handleCommandResp processes command responses. +func (s *sh2Protocol) handleCommandResp(payload []byte, timestamp uint32) { + if len(payload) < 16 { + return + } + + // seq := payload[1] + command := payload[2] + // commandSeq := payload[3] + // respSeq := payload[4] + + // Check if this response is for our command + if s.waiting && command == s.lastCmd { + s.waiting = false + // Status is in payload[6] + // For now, we just acknowledge receipt + } +} + +// handleGetFeatureResp processes get feature responses. +func (s *sh2Protocol) handleGetFeatureResp(payload []byte, timestamp uint32) { + if len(payload) < 17 { + return + } + + // Parse the response + sensorID := SensorID(payload[1]) + flags := payload[2] + changeSensitivity := binary.LittleEndian.Uint16(payload[3:5]) + reportInterval := binary.LittleEndian.Uint32(payload[5:9]) + batchInterval := binary.LittleEndian.Uint32(payload[9:13]) + sensorSpecific := binary.LittleEndian.Uint32(payload[13:17]) + + // If we're waiting for this sensor's config, store it + if s.pendingConfigRequest && s.pendingConfigSensor == sensorID { + s.receivedConfig = SensorConfig{ + ChangeSensitivityEnabled: flags&featChangeSensitivityEnabled != 0, + ChangeSensitivityRelative: flags&featChangeSensitivityRelative != 0, + WakeupEnabled: flags&featWakeEnabled != 0, + AlwaysOnEnabled: flags&featAlwaysOnEnabled != 0, + ChangeSensitivity: changeSensitivity, + ReportInterval: reportInterval, + BatchInterval: batchInterval, + SensorSpecific: sensorSpecific, + } + s.configReady = true + } +} + +// handleSensor processes sensor report messages. +// The payload can contain multiple sensor reports batched together. +func (s *sh2Protocol) handleSensor(payload []byte, timestamp uint32) { + cursor := 0 + var referenceDelta uint32 + + for cursor < len(payload) { + if cursor >= len(payload) { + break + } + + reportID := payload[cursor] + reportLen := getReportLen(reportID) + + if reportLen == 0 { + // Unknown report ID + break + } + + if cursor+reportLen > len(payload) { + // Not enough data for this report + break + } + + // Handle special report types + switch reportID { + case 0xFB: // SENSORHUB_BASE_TIMESTAMP_REF + if reportLen >= 5 { + // Extract timebase (little-endian uint32) + timebase := binary.LittleEndian.Uint32(payload[cursor+1 : cursor+5]) + referenceDelta = -timebase // Store negative for delta calculation + } + + case 0xFA: // SENSORHUB_TIMESTAMP_REBASE + if reportLen >= 5 { + timebase := binary.LittleEndian.Uint32(payload[cursor+1 : cursor+5]) + referenceDelta += timebase + } + + case 0xF1: // SENSORHUB_FLUSH_COMPLETED + // Route to control handler + s.handleControl(payload[cursor:cursor+reportLen], timestamp) + + default: + // Regular sensor report + value, ok := decodeSensor(payload[cursor:cursor+reportLen], timestamp) + if ok { + s.device.enqueue(value) + } + } + + cursor += reportLen + } +} // handleExecutable processes executable channel messages. +func (s *sh2Protocol) handleExecutable(payload []byte, timestamp uint32) { + if len(payload) == 0 { + return + } + + reportID := payload[0] + + switch reportID { + case execDeviceRespResetComplete: + s.device.lastReset = true + } +} diff --git a/bno08x/shtp.go b/bno08x/shtp.go new file mode 100644 index 0000000..20cafd8 --- /dev/null +++ b/bno08x/shtp.go @@ -0,0 +1,83 @@ +// SHTP specification found at https://www.ceva-ip.com/wp-content/uploads/SH-2-SHTP-Reference-Manual.pdf + +package bno08x + +import "encoding/binary" + +// shtpHandler is a callback for handling SHTP channel data. +type shtpHandler func(payload []byte, timestamp uint32) + +// shtp implements the Sensor Hub Transport Protocol layer. +type shtp struct { + hal *hal + handlers map[uint8]shtpHandler + seq [8]uint8 + rx [maxTransferIn]byte // Reusable receive buffer + tx [maxTransferOut]byte // Reusable transmit buffer +} + +func newSHTP(hal *hal) *shtp { + return &shtp{ + hal: hal, + handlers: make(map[uint8]shtpHandler), + } +} + +// register registers a handler for a specific SHTP channel. +func (s *shtp) register(channel uint8, handler shtpHandler) { + if handler == nil { + delete(s.handlers, channel) + return + } + s.handlers[channel] = handler +} + +// send transmits a payload on the specified channel. +func (s *shtp) send(channel uint8, payload []byte) error { + total := len(payload) + shtpHeaderLength + if total > maxTransferOut { + return errFrameTooLarge + } + + // Use pre-allocated transmit buffer to avoid allocations + frame := s.tx[:total] + binary.LittleEndian.PutUint16(frame[0:2], uint16(total)) + frame[2] = channel + frame[3] = s.seq[channel] + s.seq[channel]++ + copy(frame[shtpHeaderLength:], payload) + + _, err := s.hal.write(frame) + return err +} + +// poll checks for and processes incoming SHTP packets. +// Returns true if a packet was processed, false if no data available. +func (s *shtp) poll() (bool, error) { + n, timestamp, err := s.hal.read(s.rx[:]) + if err != nil { + return false, err + } + if n == 0 { + return false, nil + } + + packet := s.rx[:n] + length := int(binary.LittleEndian.Uint16(packet[0:2]) & ^uint16(continueMask)) + if length > n { + length = n + } + if length < shtpHeaderLength { + return false, nil + } + + channel := packet[2] + // seq := packet[3] // sequence number, not currently validated + payload := packet[shtpHeaderLength:length] + + if handler := s.handlers[channel]; handler != nil { + handler(payload, timestamp) + } + + return true, nil +} diff --git a/bno08x/types.go b/bno08x/types.go new file mode 100644 index 0000000..182774d --- /dev/null +++ b/bno08x/types.go @@ -0,0 +1,572 @@ +package bno08x + +// SensorID identifies a specific sensor type. +type SensorID uint8 + +// Sensor IDs as defined in the SH-2 specification. +const ( + SensorRawAccelerometer SensorID = 0x14 + SensorAccelerometer SensorID = 0x01 + SensorLinearAcceleration SensorID = 0x04 + SensorGravity SensorID = 0x06 + SensorRawGyroscope SensorID = 0x15 + SensorGyroscope SensorID = 0x02 + SensorGyroscopeUncalibrated SensorID = 0x07 + SensorRawMagnetometer SensorID = 0x16 + SensorMagneticField SensorID = 0x03 + SensorMagneticFieldUncalibrated SensorID = 0x0F + SensorRotationVector SensorID = 0x05 + SensorGameRotationVector SensorID = 0x08 + SensorGeomagneticRotationVector SensorID = 0x09 + SensorPressure SensorID = 0x0A + SensorAmbientLight SensorID = 0x0B + SensorHumidity SensorID = 0x0C + SensorProximity SensorID = 0x0D + SensorTemperature SensorID = 0x0E + SensorReserved SensorID = 0x17 + SensorTapDetector SensorID = 0x10 + SensorStepDetector SensorID = 0x18 + SensorStepCounter SensorID = 0x11 + SensorSignificantMotion SensorID = 0x12 + SensorStabilityClassifier SensorID = 0x13 + SensorShakeDetector SensorID = 0x19 + SensorFlipDetector SensorID = 0x1A + SensorPickupDetector SensorID = 0x1B + SensorStabilityDetector SensorID = 0x1C + SensorPersonalActivityClassifier SensorID = 0x1E + SensorSleepDetector SensorID = 0x1F + SensorTiltDetector SensorID = 0x20 + SensorPocketDetector SensorID = 0x21 + SensorCircleDetector SensorID = 0x22 + SensorHeartRateMonitor SensorID = 0x23 + SensorARVRStabilizedRV SensorID = 0x28 + SensorARVRStabilizedGRV SensorID = 0x29 + SensorGyroIntegratedRV SensorID = 0x2A + SensorIZROMotionRequest SensorID = 0x2B + SensorMaxID SensorID = 0x2B +) + +// ProductID contains firmware information from the sensor. +type ProductID struct { + ResetCause uint8 + VersionMajor uint8 + VersionMinor uint8 + PartNumber uint32 + BuildNumber uint32 + VersionPatch uint16 + Reserved0 uint8 + Reserved1 uint8 +} + +// ProductIDs holds all product ID entries returned by the sensor. +type ProductIDs struct { + Entries [5]ProductID + NumEntries uint8 +} + +// Vector3 represents a 3D vector. +type Vector3 struct { + X float32 + Y float32 + Z float32 +} + +// Quaternion represents a quaternion in (real, i, j, k) format. +// Note: This maps to (w, x, y, z) convention where w=real, x=i, y=j, z=k. +type Quaternion struct { + Real float32 + I float32 + J float32 + K float32 +} + +// RawVector3 contains raw ADC counts with timestamp. +type RawVector3 struct { + X int16 + Y int16 + Z int16 + Timestamp uint32 +} + +// RawGyroscope contains raw gyro readings with temperature and timestamp. +type RawGyroscope struct { + X int16 + Y int16 + Z int16 + Temperature int16 + Timestamp uint32 +} + +// GyroscopeUncalibrated contains uncalibrated gyroscope data with bias. +type GyroscopeUncalibrated struct { + X float32 + Y float32 + Z float32 + BiasX float32 + BiasY float32 + BiasZ float32 +} + +// MagneticFieldUncalibrated contains uncalibrated magnetometer data with bias. +type MagneticFieldUncalibrated struct { + X float32 + Y float32 + Z float32 + BiasX float32 + BiasY float32 + BiasZ float32 +} + +// TapDetector contains tap/double-tap detection flags. +type TapDetector struct { + Flags uint8 +} + +// StepDetector contains step detection with latency. +type StepDetector struct { + Latency uint32 +} + +// StepCounter contains step count with latency. +type StepCounter struct { + Count uint16 + Latency uint32 +} + +// SignificantMotion indicates significant motion was detected. +type SignificantMotion struct { + Motion uint16 +} + +// ActivityClassification contains activity classification data. +type ActivityClassification struct { + Page uint8 + MostLikelyState uint8 + Classification [10]uint8 + EndOfPage uint8 +} + +// ShakeDetector contains shake detection data. +type ShakeDetector struct { + Shake uint16 +} + +// StabilityClassifier contains stability classification. +type StabilityClassifier struct { + Classification uint8 +} + +// PersonalActivityClassifier contains personal activity data. +type PersonalActivityClassifier struct { + Page uint8 + MostLikelyState uint8 + Confidence [10]uint8 + EndOfPage uint8 +} + +// SensorValue contains decoded sensor data for all sensor types. +type SensorValue struct { + id SensorID + status uint8 + sequence uint8 + delay uint8 + timestamp uint64 + + // Orientation data (quaternions) + quaternion Quaternion + quaternionAccuracy float32 + + // Linear measurements + accelerometer Vector3 + linearAcceleration Vector3 + gravity Vector3 + gyroscope Vector3 + gyroscopeUncal GyroscopeUncalibrated + magneticField Vector3 + magneticFieldUncal MagneticFieldUncalibrated + + // Raw sensor data + rawAccelerometer RawVector3 + rawGyroscope RawGyroscope + rawMagnetometer RawVector3 + + // Environmental sensors + pressure float32 // hPa + ambientLight float32 // lux + humidity float32 // % + proximity float32 // cm + temperature float32 // °C + + // Activity detection + tapDetector TapDetector + stepCounter StepCounter + stepDetector StepDetector + significantMotion SignificantMotion + shakeDetector ShakeDetector + flipDetector uint16 + stabilityClassifier StabilityClassifier + stabilityDetector uint8 + activityClassifier ActivityClassification + personalActivityClassifier PersonalActivityClassifier + sleepDetector uint8 + tiltDetector uint8 + pocketDetector uint8 + circleDetector uint8 + heartRateMonitor uint16 +} + +// SensorConfig holds configuration settings for a sensor. +type SensorConfig struct { + ChangeSensitivityEnabled bool + ChangeSensitivityRelative bool + WakeupEnabled bool + AlwaysOnEnabled bool + ChangeSensitivity uint16 + ReportInterval uint32 // microseconds + BatchInterval uint32 // microseconds + SensorSpecific uint32 +} + +// Error represents a driver error. +type Error string + +func (e Error) Error() string { return string(e) } + +// Error constants. +var ( + errBufferTooSmall = Error("bno08x: buffer too small") + errNoEvent = Error("bno08x: no sensor event available") + errTimeout = Error("bno08x: operation timed out") + errFrameTooLarge = Error("bno08x: frame exceeds maximum size") + errNoBus = Error("bno08x: I2C bus not configured") + errInvalidParam = Error("bno08x: invalid parameter") + errHubError = Error("bno08x: sensor hub error") + errIO = Error("bno08x: I/O error") +) + +// Metadata accessor methods (always available for any sensor type) + +// ID returns the sensor ID. +func (sv SensorValue) ID() SensorID { + return sv.id +} + +// Status returns the sensor status flags. +func (sv SensorValue) Status() uint8 { + return sv.status +} + +// Sequence returns the sequence number. +func (sv SensorValue) Sequence() uint8 { + return sv.sequence +} + +// Delay returns the sensor delay value. +func (sv SensorValue) Delay() uint8 { + return sv.delay +} + +// Timestamp returns the sensor timestamp. +func (sv SensorValue) Timestamp() uint64 { + return sv.timestamp +} + +// Orientation data accessor methods + +// Quaternion returns the quaternion value for rotation vector sensors. +// Panics if called on a sensor type that doesn't provide quaternion data. +func (sv SensorValue) Quaternion() Quaternion { + switch sv.id { + case SensorRotationVector, SensorGameRotationVector, SensorGeomagneticRotationVector, + SensorARVRStabilizedRV, SensorARVRStabilizedGRV, SensorGyroIntegratedRV: + return sv.quaternion + default: + panic("bno08x: Quaternion() called on non-rotation sensor type") + } +} + +// QuaternionAccuracy returns the quaternion accuracy estimate. +// Panics if called on a sensor type that doesn't provide quaternion accuracy. +func (sv SensorValue) QuaternionAccuracy() float32 { + switch sv.id { + case SensorRotationVector, SensorGeomagneticRotationVector, SensorARVRStabilizedRV: + return sv.quaternionAccuracy + default: + panic("bno08x: QuaternionAccuracy() called on sensor type without accuracy data") + } +} + +// Linear measurement accessor methods + +// Accelerometer returns the accelerometer vector. +// Panics if called on a sensor type other than SensorAccelerometer. +func (sv SensorValue) Accelerometer() Vector3 { + if sv.id != SensorAccelerometer { + panic("bno08x: Accelerometer() called on non-accelerometer sensor type") + } + return sv.accelerometer +} + +// LinearAcceleration returns the linear acceleration vector. +// Panics if called on a sensor type other than SensorLinearAcceleration. +func (sv SensorValue) LinearAcceleration() Vector3 { + if sv.id != SensorLinearAcceleration { + panic("bno08x: LinearAcceleration() called on wrong sensor type") + } + return sv.linearAcceleration +} + +// Gravity returns the gravity vector. +// Panics if called on a sensor type other than SensorGravity. +func (sv SensorValue) Gravity() Vector3 { + if sv.id != SensorGravity { + panic("bno08x: Gravity() called on non-gravity sensor type") + } + return sv.gravity +} + +// Gyroscope returns the gyroscope vector. +// Panics if called on a sensor type other than SensorGyroscope. +func (sv SensorValue) Gyroscope() Vector3 { + if sv.id != SensorGyroscope { + panic("bno08x: Gyroscope() called on non-gyroscope sensor type") + } + return sv.gyroscope +} + +// GyroscopeUncal returns the uncalibrated gyroscope data. +// Panics if called on a sensor type other than SensorGyroscopeUncalibrated. +func (sv SensorValue) GyroscopeUncal() GyroscopeUncalibrated { + if sv.id != SensorGyroscopeUncalibrated { + panic("bno08x: GyroscopeUncal() called on wrong sensor type") + } + return sv.gyroscopeUncal +} + +// MagneticField returns the magnetic field vector. +// Panics if called on a sensor type other than SensorMagneticField. +func (sv SensorValue) MagneticField() Vector3 { + if sv.id != SensorMagneticField { + panic("bno08x: MagneticField() called on wrong sensor type") + } + return sv.magneticField +} + +// MagneticFieldUncal returns the uncalibrated magnetic field data. +// Panics if called on a sensor type other than SensorMagneticFieldUncalibrated. +func (sv SensorValue) MagneticFieldUncal() MagneticFieldUncalibrated { + if sv.id != SensorMagneticFieldUncalibrated { + panic("bno08x: MagneticFieldUncal() called on wrong sensor type") + } + return sv.magneticFieldUncal +} + +// Raw sensor data accessor methods + +// RawAccelerometer returns the raw accelerometer data. +// Panics if called on a sensor type other than SensorRawAccelerometer. +func (sv SensorValue) RawAccelerometer() RawVector3 { + if sv.id != SensorRawAccelerometer { + panic("bno08x: RawAccelerometer() called on wrong sensor type") + } + return sv.rawAccelerometer +} + +// RawGyroscope returns the raw gyroscope data. +// Panics if called on a sensor type other than SensorRawGyroscope. +func (sv SensorValue) RawGyroscope() RawGyroscope { + if sv.id != SensorRawGyroscope { + panic("bno08x: RawGyroscope() called on wrong sensor type") + } + return sv.rawGyroscope +} + +// RawMagnetometer returns the raw magnetometer data. +// Panics if called on a sensor type other than SensorRawMagnetometer. +func (sv SensorValue) RawMagnetometer() RawVector3 { + if sv.id != SensorRawMagnetometer { + panic("bno08x: RawMagnetometer() called on wrong sensor type") + } + return sv.rawMagnetometer +} + +// Environmental sensor accessor methods + +// Pressure returns the pressure reading in hPa. +// Panics if called on a sensor type other than SensorPressure. +func (sv SensorValue) Pressure() float32 { + if sv.id != SensorPressure { + panic("bno08x: Pressure() called on non-pressure sensor type") + } + return sv.pressure +} + +// AmbientLight returns the ambient light reading in lux. +// Panics if called on a sensor type other than SensorAmbientLight. +func (sv SensorValue) AmbientLight() float32 { + if sv.id != SensorAmbientLight { + panic("bno08x: AmbientLight() called on wrong sensor type") + } + return sv.ambientLight +} + +// Humidity returns the humidity reading in percent. +// Panics if called on a sensor type other than SensorHumidity. +func (sv SensorValue) Humidity() float32 { + if sv.id != SensorHumidity { + panic("bno08x: Humidity() called on non-humidity sensor type") + } + return sv.humidity +} + +// Proximity returns the proximity reading in cm. +// Panics if called on a sensor type other than SensorProximity. +func (sv SensorValue) Proximity() float32 { + if sv.id != SensorProximity { + panic("bno08x: Proximity() called on non-proximity sensor type") + } + return sv.proximity +} + +// Temperature returns the temperature reading in °C. +// Panics if called on a sensor type other than SensorTemperature. +func (sv SensorValue) Temperature() float32 { + if sv.id != SensorTemperature { + panic("bno08x: Temperature() called on non-temperature sensor type") + } + return sv.temperature +} + +// Activity detection accessor methods + +// TapDetector returns the tap detector data. +// Panics if called on a sensor type other than SensorTapDetector. +func (sv SensorValue) TapDetector() TapDetector { + if sv.id != SensorTapDetector { + panic("bno08x: TapDetector() called on wrong sensor type") + } + return sv.tapDetector +} + +// StepCounter returns the step counter value. +// Panics if called on a sensor type other than SensorStepCounter. +func (sv SensorValue) StepCounter() StepCounter { + if sv.id != SensorStepCounter { + panic("bno08x: StepCounter() called on wrong sensor type") + } + return sv.stepCounter +} + +// StepDetector returns the step detector data. +// Panics if called on a sensor type other than SensorStepDetector. +func (sv SensorValue) StepDetector() StepDetector { + if sv.id != SensorStepDetector { + panic("bno08x: StepDetector() called on wrong sensor type") + } + return sv.stepDetector +} + +// SignificantMotion returns the significant motion data. +// Panics if called on a sensor type other than SensorSignificantMotion. +func (sv SensorValue) SignificantMotion() SignificantMotion { + if sv.id != SensorSignificantMotion { + panic("bno08x: SignificantMotion() called on wrong sensor type") + } + return sv.significantMotion +} + +// ShakeDetector returns the shake detector data. +// Panics if called on a sensor type other than SensorShakeDetector. +func (sv SensorValue) ShakeDetector() ShakeDetector { + if sv.id != SensorShakeDetector { + panic("bno08x: ShakeDetector() called on wrong sensor type") + } + return sv.shakeDetector +} + +// FlipDetector returns the flip detector data. +// Panics if called on a sensor type other than SensorFlipDetector. +func (sv SensorValue) FlipDetector() uint16 { + if sv.id != SensorFlipDetector { + panic("bno08x: FlipDetector() called on wrong sensor type") + } + return sv.flipDetector +} + +// StabilityClassifier returns the stability classifier data. +// Panics if called on a sensor type other than SensorStabilityClassifier. +func (sv SensorValue) StabilityClassifier() StabilityClassifier { + if sv.id != SensorStabilityClassifier { + panic("bno08x: StabilityClassifier() called on wrong sensor type") + } + return sv.stabilityClassifier +} + +// StabilityDetector returns the stability detector value. +// Panics if called on a sensor type other than SensorStabilityDetector. +func (sv SensorValue) StabilityDetector() uint8 { + if sv.id != SensorStabilityDetector { + panic("bno08x: StabilityDetector() called on wrong sensor type") + } + return sv.stabilityDetector +} + +// ActivityClassifier returns the activity classification data. +// Note: This field appears unused in decode.go, keeping for API compatibility. +func (sv SensorValue) ActivityClassifier() ActivityClassification { + return sv.activityClassifier +} + +// PersonalActivityClassifier returns the personal activity classifier data. +// Panics if called on a sensor type other than SensorPersonalActivityClassifier. +func (sv SensorValue) PersonalActivityClassifier() PersonalActivityClassifier { + if sv.id != SensorPersonalActivityClassifier { + panic("bno08x: PersonalActivityClassifier() called on wrong sensor type") + } + return sv.personalActivityClassifier +} + +// SleepDetector returns the sleep detector value. +// Panics if called on a sensor type other than SensorSleepDetector. +func (sv SensorValue) SleepDetector() uint8 { + if sv.id != SensorSleepDetector { + panic("bno08x: SleepDetector() called on wrong sensor type") + } + return sv.sleepDetector +} + +// TiltDetector returns the tilt detector value. +// Panics if called on a sensor type other than SensorTiltDetector. +func (sv SensorValue) TiltDetector() uint8 { + if sv.id != SensorTiltDetector { + panic("bno08x: TiltDetector() called on wrong sensor type") + } + return sv.tiltDetector +} + +// PocketDetector returns the pocket detector value. +// Panics if called on a sensor type other than SensorPocketDetector. +func (sv SensorValue) PocketDetector() uint8 { + if sv.id != SensorPocketDetector { + panic("bno08x: PocketDetector() called on wrong sensor type") + } + return sv.pocketDetector +} + +// CircleDetector returns the circle detector value. +// Panics if called on a sensor type other than SensorCircleDetector. +func (sv SensorValue) CircleDetector() uint8 { + if sv.id != SensorCircleDetector { + panic("bno08x: CircleDetector() called on wrong sensor type") + } + return sv.circleDetector +} + +// HeartRateMonitor returns the heart rate monitor value. +// Panics if called on a sensor type other than SensorHeartRateMonitor. +func (sv SensorValue) HeartRateMonitor() uint16 { + if sv.id != SensorHeartRateMonitor { + panic("bno08x: HeartRateMonitor() called on wrong sensor type") + } + return sv.heartRateMonitor +} diff --git a/examples/bno08x/i2c/main.go b/examples/bno08x/i2c/main.go new file mode 100644 index 0000000..8575434 --- /dev/null +++ b/examples/bno08x/i2c/main.go @@ -0,0 +1,66 @@ +// Package main provides a basic example of using the BNO08x driver +// to read rotation vector (quaternion) data from the sensor. +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/bno08x" +) + +func main() { + time.Sleep(2 * time.Second) // Wait for sensor to power up + // Initialize I2C bus + i2c := machine.I2C0 + err := i2c.Configure(machine.I2CConfig{ + Frequency: 400 * machine.KHz, + }) + if err != nil { + println("Failed to configure I2C:", err.Error()) + return + } + + println("Initializing BNO08x sensor...") + + // Create and configure sensor using I2C + sensor := bno08x.NewI2C(i2c) + err = sensor.Configure(bno08x.Config{}) + if err != nil { + println("Failed to configure sensor:", err.Error()) + return + } + + println("Sensor initialized successfully") + + // Enable Game Rotation Vector reports at 100Hz (10000 microseconds = 10ms interval) + // Using Game Rotation Vector (0x08) to match the working channel_debug test + err = sensor.EnableReport(bno08x.SensorGameRotationVector, 10000) + if err != nil { + println("Failed to enable game rotation vector:", err.Error()) + return + } + + println("Reading rotation vectors...") + println("Format: Real I J K Accuracy") + + // Add a delay after enabling reports (Arduino does this) + time.Sleep(100 * time.Millisecond) + + // Main loop - read and display quaternion data + for { + event, ok := sensor.GetSensorEvent() + if ok && (event.ID() == bno08x.SensorRotationVector || event.ID() == bno08x.SensorGameRotationVector) { + q := event.Quaternion() + if event.ID() == bno08x.SensorRotationVector { + println(q.Real, q.I, q.J, q.K, event.QuaternionAccuracy()) + } else { + // GameRotationVector doesn't have accuracy + println(q.Real, q.I, q.J, q.K) + } + } + + // Arduino uses 10ms delay in loop + time.Sleep(10 * time.Millisecond) + } +} diff --git a/smoketest.sh b/smoketest.sh index 29b3ce3..aee7229 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -20,6 +20,7 @@ tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmi tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmp180/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmp280/main.go tinygo build -size short -o ./build/test.hex -target=trinket-m0 ./examples/bmp388/main.go +tinygo build -size short -o ./build/test.hex -target=metro-rp2350 ./examples/bno08x/i2c/main.go tinygo build -size short -o ./build/test.hex -target=bluepill ./examples/ds1307/sram/main.go tinygo build -size short -o ./build/test.hex -target=bluepill ./examples/ds1307/time/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/ds3231/alarms/main.go From 50778af656b6fca009f4c816fcc4892f23ae82a1 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 7 Dec 2025 19:39:26 +0100 Subject: [PATCH 106/137] si5351: add many missing functions needed for convenient use. This adds many missing functions needed for convenient use that are reinterpreted from the C code in https://github.com/dmalnati/picoinf Thank you! Signed-off-by: deadprogram --- examples/si5351/main.go | 21 +- si5351/registers.go | 15 ++ si5351/si5351.go | 439 ++++++++++++++++++++++++++++------------ 3 files changed, 342 insertions(+), 133 deletions(-) diff --git a/examples/si5351/main.go b/examples/si5351/main.go index bd49ab0..d7480a0 100644 --- a/examples/si5351/main.go +++ b/examples/si5351/main.go @@ -97,11 +97,24 @@ func main() { // After configuring PLLs and clocks, enable the outputs. clockgen.EnableOutputs() + time.Sleep(time.Second) + + clockgen.DisableOutputs() + println("All outputs disabled for 5 seconds") + time.Sleep(5 * time.Second) + + // Now use SetFrequency to re-set the frequencies of the outputs + on := false for { + if on { + println("Setting Clock 0 output off") + clockgen.OutputEnable(0, false) + on = false + } else { + println("Setting Clock 0 output to 100mhz") + clockgen.SetFrequency(100*machine.MHz, 0, si5351.PLL_A) + on = true + } time.Sleep(5 * time.Second) - println() - println("Clock 0: 112.5mhz") - println("Clock 1: 13.5531mhz") - println("Clock 2: 10.706khz") } } diff --git a/si5351/registers.go b/si5351/registers.go index 79300ad..1cb8a49 100644 --- a/si5351/registers.go +++ b/si5351/registers.go @@ -62,3 +62,18 @@ const ( MULTISYNTH_DIV_6 = 6 MULTISYNTH_DIV_8 = 8 ) + +// Frequency constants (in Hz) +const ( + CLKOUT_MIN_FREQ = 8000 // 8 kHz + CLKOUT_MAX_FREQ = 150000000 // 150 MHz + MULTISYNTH_MAX_FREQ = 150000000 // 150 MHz + MULTISYNTH_SHARE_MAX = 100000000 // 100 MHz + MULTISYNTH_DIVBY4_FREQ = 150000000 // 150 MHz + PLL_VCO_MIN = 600000000 // 600 MHz + PLL_VCO_MAX = 900000000 // 900 MHz +) + +const ( + SI5351_PLL_C_MAX = 1048575 +) diff --git a/si5351/si5351.go b/si5351/si5351.go index e7cc623..55c8f2d 100644 --- a/si5351/si5351.go +++ b/si5351/si5351.go @@ -134,6 +134,23 @@ func (d *Device) DisableOutputs() error { return d.rw.Write8(OUTPUT_ENABLE_CONTROL, 0xFF) } +// packRegSet packs P1, P2, P3 values into the 8-byte register format +// used by both PLL and Multisynth configuration. +// For multisynth, rDivBits should contain the R divider value shifted left by 4. +// For PLL, rDivBits should be 0. +func packRegSet(p1, p2, p3 uint32, rDivBits uint8) [8]byte { + var data [8]byte + data[0] = uint8((p3 & 0xFF00) >> 8) + data[1] = uint8(p3 & 0xFF) + data[2] = uint8((p1&0x30000)>>16) | rDivBits + data[3] = uint8((p1 & 0xFF00) >> 8) + data[4] = uint8(p1 & 0xFF) + data[5] = uint8(((p3 & 0xF0000) >> 12) | ((p2 & 0xF0000) >> 16)) + data[6] = uint8((p2 & 0xFF00) >> 8) + data[7] = uint8(p2 & 0xFF) + return data +} + // ConfigurePLL sets the multiplier for the specified PLL // pll The PLL to configure, which must be one of the following: // - PLL_A @@ -163,42 +180,24 @@ func (d *Device) DisableOutputs() error { // See: http://www.silabs.com/Support%20Documents/TechnicalDocs/AN619.pdf func (d *Device) ConfigurePLL(pll uint8, mult uint8, num uint32, denom uint32) error { // Basic validation - if !d.initialised { + switch { + case !d.initialised: return ErrNotInitialised - } // mult = 15..90 - if !((mult > 14) && (mult < 91)) { + case !((mult > 14) && (mult < 91)): return ErrInvalidParameter - } // Avoid divide by zero - if !(denom > 0) { + case !(denom > 0): return ErrInvalidParameter - } // 20-bit limit - if !(num <= 0xFFFFF) { + case !(num <= 0xFFFFF): return ErrInvalidParameter - } // 20-bit limit - if !(denom <= 0xFFFFF) { + case !(denom <= 0xFFFFF): return ErrInvalidParameter } - // PLL Multiplier Equations - // - // P1 register is an 18-bit value using following formula: - // - // P1[17:0] = 128 * mult + floor(128*(num/denom)) - 512 - // - // P2 register is a 20-bit value using the following formula: - // - // P2[19:0] = 128 * num - denom * floor(128*(num/denom)) - // - // P3 register is a 20-bit value using the following formula: - // - // P3[19:0] = denom - // - - // Set PLL config registers + // Calculate PLL register values var p1, p2, p3 uint32 if num == 0 { // Integer mode @@ -218,16 +217,8 @@ func (d *Device) ConfigurePLL(pll uint8, mult uint8, num uint32, denom uint32) e baseaddr = 34 } - // The datasheet is a nightmare of typos and inconsistencies here! - data := [8]byte{} - data[0] = uint8((p3 & 0x0000FF00) >> 8) - data[1] = uint8(p3 & 0x000000FF) - data[2] = uint8((p1 & 0x00030000) >> 16) - data[3] = uint8((p1 & 0x0000FF00) >> 8) - data[4] = uint8(p1 & 0x000000FF) - data[5] = uint8(((p3 & 0x000F0000) >> 12) | ((p2 & 0x000F0000) >> 16)) - data[6] = uint8((p2 & 0x0000FF00) >> 8) - data[7] = uint8(p2 & 0x000000FF) + // Pack and write registers + data := packRegSet(p1, p2, p3, 0) if err := d.bus.Tx(uint16(baseaddr), data[:], nil); err != nil { return err } @@ -305,120 +296,58 @@ func (d *Device) ConfigurePLL(pll uint8, mult uint8, num uint32, denom uint32) e // used, but this isn't currently implemented in the driver. func (d *Device) ConfigureMultisynth(output uint8, pll uint8, div uint32, num uint32, denom uint32) error { // Basic validation - if !d.initialised { + switch { + case !d.initialised: return ErrNotInitialised - } // Channel range - if !(output < 3) { + case !(output < 3): return fmt.Errorf("output channel must be between 0 and 2") - } // Divider integer value - if !((div > 3) && (div < 2049)) { + case !((div > 3) && (div < 2049)): return ErrInvalidParameter - } // Avoid divide by zero - if !(denom > 0) { + case !(denom > 0): return ErrInvalidParameter - } // 20-bit limit - if !(num <= 0xFFFFF) { + case !(num <= 0xFFFFF): return ErrInvalidParameter - } // 20-bit limit - if !(denom <= 0xFFFFF) { + case !(denom <= 0xFFFFF): + return ErrInvalidParameter + // Make sure the requested PLL has been initialised + case pll == PLL_A && !d.pllaConfigured: + return ErrInvalidParameter + case pll == PLL_B && !d.pllbConfigured: return ErrInvalidParameter } - // Make sure the requested PLL has been initialised - if pll == PLL_A && !d.pllaConfigured { - return ErrInvalidParameter - } - if pll == PLL_B && !d.pllbConfigured { - return ErrInvalidParameter - } - - // Output Multisynth Divider Equations - // - // where: a = div, b = num and c = denom - // - // P1 register is an 18-bit value using following formula: - // - // P1[17:0] = 128 * a + floor(128*(b/c)) - 512 - // - // P2 register is a 20-bit value using the following formula: - // - // P2[19:0] = 128 * b - c * floor(128*(b/c)) - // - // P3 register is a 20-bit value using the following formula: - // - // P3[19:0] = c - // - - // Set PLL config registers - var p1, p2, p3 uint32 - if num == 0 { + // Calculate register values + var reg si5351RegSet + switch { + case num == 0: // Integer mode - p1 = 128*div - 512 - p2 = 0 - p3 = denom - } else if denom == 1 { + reg.p1 = 128*div - 512 + reg.p2 = 0 + reg.p3 = denom + case denom == 1: // Fractional mode, simplified calculations - p1 = 128*div + 128*num - 512 - p2 = 128*num - 128 - p3 = 1 - } else { + reg.p1 = 128*div + 128*num - 512 + reg.p2 = 128*num - 128 + reg.p3 = 1 + default: // Fractional mode - p1 = uint32(128*float64(div) + math.Floor(128*(float64(num)/float64(denom))) - 512) - p2 = uint32(128*float64(num) - float64(denom)*math.Floor(128*(float64(num)/float64(denom)))) - p3 = denom + reg.p1 = uint32(128*float64(div) + math.Floor(128*(float64(num)/float64(denom))) - 512) + reg.p2 = uint32(128*float64(num) - float64(denom)*math.Floor(128*(float64(num)/float64(denom)))) + reg.p3 = denom } - // Get the appropriate starting point for the PLL registers - baseaddr := uint8(0) - switch output { - case 0: - baseaddr = MULTISYNTH0_PARAMETERS_1 - case 1: - baseaddr = MULTISYNTH1_PARAMETERS_1 - case 2: - baseaddr = MULTISYNTH2_PARAMETERS_1 - } + // Determine if we should use integer mode + intMode := num == 0 - // Set the MSx config registers - data := [8]byte{} - data[0] = uint8((p3 & 0xFF00) >> 8) - data[1] = uint8(p3 & 0xFF) - data[2] = uint8(((p1 & 0x30000) >> 16)) | d.lastRdivValue[output] - data[3] = uint8((p1 & 0xFF00) >> 8) - data[4] = uint8(p1 & 0xFF) - data[5] = uint8(((p3 & 0xF0000) >> 12) | ((p2 & 0xF0000) >> 16)) - data[6] = uint8((p2 & 0xFF00) >> 8) - data[7] = uint8(p2 & 0xFF) - if err := d.bus.Tx(uint16(baseaddr), data[:], nil); err != nil { - return err - } + // Use existing R divider value (0 if not previously set) + rDiv := d.lastRdivValue[output] >> 4 - // Configure the clk control and enable the output - // TODO: Check if the clk control byte needs to be updated. - clkControlReg := uint8(0x0F) // 8mA drive strength, MS0 as CLK0 source, Clock not inverted, powered up - if pll == PLL_B { - clkControlReg |= (1 << 5) // Uses PLLB - } - if num == 0 { - clkControlReg |= (1 << 6) // Integer mode - } - - var register uint8 - switch output { - case 0: - register = CLK0_CONTROL - case 1: - register = CLK1_CONTROL - case 2: - register = CLK2_CONTROL - } - - return d.rw.Write8(register, clkControlReg) + return d.setMS(output, reg, intMode, rDiv, pll) } func (d *Device) ConfigureRdiv(output uint8, div uint8) error { @@ -446,3 +375,255 @@ func (d *Device) ConfigureRdiv(output uint8, div uint8) error { data = (data & 0x0F) | d.lastRdivValue[output] return d.rw.Write8(register, data) } + +// si5351RegSet holds the register values for multisynth configuration +type si5351RegSet struct { + p1 uint32 + p2 uint32 + p3 uint32 +} + +var ErrFrequencyOutOfRange = errors.New("Si5351 frequency out of range") +var ErrClockConflict = errors.New("Si5351 clock conflict with existing configuration") + +// SetFrequency sets the clock frequency of the specified CLK output. +// Frequency range is 8 kHz to 150 MHz. +// +// freq - Output frequency in Hz +// output - Clock output (0, 1, or 2 for this driver) +// pll - The PLL to use (PLL_A or PLL_B) +func (d *Device) SetFrequency(freq uint64, output uint8, pll uint8) error { + switch { + case !d.initialised: + return ErrNotInitialised + case output > 2: + return ErrInvalidParameter + } + + switch { + // Lower bounds check + case freq < CLKOUT_MIN_FREQ: + freq = CLKOUT_MIN_FREQ + // Upper bounds check + case freq > MULTISYNTH_MAX_FREQ: + freq = MULTISYNTH_MAX_FREQ + } + + // Select the proper R divider value for low frequencies + rDiv := d.selectRDiv(&freq) + + // Calculate PLL and multisynth parameters + var pllFreq uint64 + switch { + case pll == PLL_A && d.pllaConfigured: + pllFreq = uint64(d.pllaFreq) + case pll == PLL_B && d.pllbConfigured: + pllFreq = uint64(d.pllbFreq) + default: + // PLL not configured, calculate optimal PLL frequency + pllFreq = d.calculatePLLFreq(freq) + } + + // Calculate multisynth divider parameters + msReg := d.multisynthCalc(freq, pllFreq) + + // Determine if we should use integer mode + intMode := msReg.p2 == 0 + + // Configure PLL if not already configured or if we need a new frequency + if (pll == PLL_A && !d.pllaConfigured) || (pll == PLL_B && !d.pllbConfigured) { + if err := d.setPLL(pllFreq, pll); err != nil { + return err + } + } + + // Set multisynth registers + if err := d.setMS(output, msReg, intMode, rDiv, pll); err != nil { + return err + } + + // Enable output + return d.OutputEnable(output, true) +} + +// selectRDiv selects the appropriate R divider for low frequencies +// and modifies the frequency accordingly +func (d *Device) selectRDiv(freq *uint64) uint8 { + var rDiv uint8 = 0 + + if *freq >= CLKOUT_MIN_FREQ && *freq < CLKOUT_MIN_FREQ*2 { + rDiv = R_DIV_128 + *freq *= 128 + } else if *freq >= CLKOUT_MIN_FREQ*2 && *freq < CLKOUT_MIN_FREQ*4 { + rDiv = R_DIV_64 + *freq *= 64 + } else if *freq >= CLKOUT_MIN_FREQ*4 && *freq < CLKOUT_MIN_FREQ*8 { + rDiv = R_DIV_32 + *freq *= 32 + } else if *freq >= CLKOUT_MIN_FREQ*8 && *freq < CLKOUT_MIN_FREQ*16 { + rDiv = R_DIV_16 + *freq *= 16 + } else if *freq >= CLKOUT_MIN_FREQ*16 && *freq < CLKOUT_MIN_FREQ*32 { + rDiv = R_DIV_8 + *freq *= 8 + } else if *freq >= CLKOUT_MIN_FREQ*32 && *freq < CLKOUT_MIN_FREQ*64 { + rDiv = R_DIV_4 + *freq *= 4 + } else if *freq >= CLKOUT_MIN_FREQ*64 && *freq < CLKOUT_MIN_FREQ*128 { + rDiv = R_DIV_2 + *freq *= 2 + } + + return rDiv +} + +// calculatePLLFreq calculates an optimal PLL frequency for the given output frequency +func (d *Device) calculatePLLFreq(freq uint64) uint64 { + // Try to find an integer divider that puts PLL in valid range (600-900 MHz) + // Start with a divider that gives us a PLL freq near 750 MHz (middle of range) + targetPLL := uint64(750000000) + divider := targetPLL / freq + + // Ensure divider is in valid range (8-900 for fractional, 4/6/8 for integer) + + switch { + case divider < 8: + divider = 8 + case divider > 900: + divider = 900 + } + + pllFreq := freq * divider + + // Ensure PLL frequency is in valid range + switch { + case pllFreq < PLL_VCO_MIN: + pllFreq = PLL_VCO_MIN + case pllFreq > PLL_VCO_MAX: + pllFreq = PLL_VCO_MAX + } + + return pllFreq +} + +// multisynthCalc calculates the multisynth register values +func (d *Device) multisynthCalc(freq, pllFreq uint64) si5351RegSet { + var reg si5351RegSet + + // Calculate the division ratio + // divider = pllFreq / freq + a := uint32(pllFreq / freq) + remainder := pllFreq % freq + + // Calculate b and c for fractional part + // We use c = SI5351_PLL_C_MAX (max 20-bit value) for best resolution + c := uint32(SI5351_PLL_C_MAX) + b := uint32((uint64(remainder) * uint64(c)) / freq) + + // Calculate P1, P2, P3 + // P1 = 128 * a + floor(128 * b / c) - 512 + // P2 = 128 * b - c * floor(128 * b / c) + // P3 = c + floor128bc := uint32((128 * uint64(b)) / uint64(c)) + + reg.p1 = 128*a + floor128bc - 512 + reg.p2 = 128*b - c*floor128bc + reg.p3 = c + + return reg +} + +// setPLL configures the PLL with the specified frequency +func (d *Device) setPLL(pllFreq uint64, pll uint8) error { + // Calculate PLL multiplier from crystal frequency + // pllFreq = crystalFreq * (a + b/c) + xtalFreq := uint64(d.crystalFreq) + + a := uint32(pllFreq / xtalFreq) + remainder := pllFreq % xtalFreq + + // Use max denominator for best resolution + c := uint32(SI5351_PLL_C_MAX) + b := uint32((remainder * uint64(c)) / xtalFreq) + + return d.ConfigurePLL(pll, uint8(a), b, c) +} + +// setMS sets the multisynth registers for the specified output +func (d *Device) setMS(output uint8, reg si5351RegSet, intMode bool, rDiv uint8, pll uint8) error { + // Get the appropriate starting point for the registers + var baseaddr uint8 + switch output { + case 0: + baseaddr = MULTISYNTH0_PARAMETERS_1 + case 1: + baseaddr = MULTISYNTH1_PARAMETERS_1 + case 2: + baseaddr = MULTISYNTH2_PARAMETERS_1 + default: + return ErrInvalidParameter + } + + // Store R divider value + d.lastRdivValue[output] = (rDiv & 0x07) << 4 + + // Pack and write registers + data := packRegSet(reg.p1, reg.p2, reg.p3, d.lastRdivValue[output]) + if err := d.bus.Tx(uint16(baseaddr), data[:], nil); err != nil { + return err + } + + // Configure the clk control register + clkControlReg := uint8(0x0F) // 8mA drive strength, powered up + if pll == PLL_B { + clkControlReg |= (1 << 5) // Use PLLB + } + if intMode { + clkControlReg |= (1 << 6) // Integer mode + } + + var clkReg uint8 + switch output { + case 0: + clkReg = CLK0_CONTROL + case 1: + clkReg = CLK1_CONTROL + case 2: + clkReg = CLK2_CONTROL + } + + return d.rw.Write8(clkReg, clkControlReg) +} + +// GetFreqStep returns the frequency step size of the radio in Hz. +// This is the smallest frequency increment that can be achieved, +// determined by the PLL frequency and denominator resolution. +// If pll is PLL_A, uses PLLA settings; if PLL_B, uses PLLB settings. +// Returns 0 if the specified PLL is not configured. +func (d *Device) GetFreqStep(pll uint8) uint64 { + // The frequency step at the output is: + // step = pllFreq / (SI5351_PLL_C_MAX * multisynth_divider) + // + // However, since multisynth divider varies per output, we return + // the base step from the PLL, which is: + // step = pllFreq / SI5351_PLL_C_MAX + + var pllFreq uint64 + + switch pll { + case PLL_A: + if !d.pllaConfigured { + return 0 + } + pllFreq = uint64(d.pllaFreq) + case PLL_B: + if !d.pllbConfigured { + return 0 + } + pllFreq = uint64(d.pllbFreq) + default: + return 0 + } + + return pllFreq / SI5351_PLL_C_MAX +} From f931ad44fb97bbeff7f5c4fb5dca80ce090c7da5 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Mon, 15 Dec 2025 12:14:56 +0100 Subject: [PATCH 107/137] Release 0.34 Signed-off-by: deadprogram --- CHANGELOG.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 2 +- version.go | 2 +- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f60ded4..e93ba19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,56 @@ +0.34.0 +--- +- **core** + - add regmap package to facilitate heapless driver development + - PinInput+PinOutput HAL (#753, reloaded) (#795) + - Add Device8I2C/SPI types and their logic (#801) + +- **new devices** + - **bno8x** + - Add support for CEVA BNO08x 9DoF sensor (#809) + - **hineyhsc** + - Add Honeywell HSC TruStability SPI+I2C pressure sensor driver (#799) + - **p25q16h** + - added support for P25Q16H flash chip for xiao-ble target + - **si5351** + - add support for si5351 (#810) + - **w25q80dv** + - added support for W25Q80DV flash chip for xiao-ble target + - **w5500** + - initial version the driver (#788) + +- **enhancements** + - **ds3231** + - DS3231 Alarm features (#805) + - **general** + - add simplest driver ports + - **lis3dh** + - add Update and Acceleration calls + - use correct error handling and make configurable + - **lsm9ds1** + - avoid unnecessary heap allocations + - **pixel** + - add Grayscale2bit color (#817) + - **scd4x** + - add support for SCD41 single-shot measurements + - remove dead code + - update package to use standard methods + - **si5351** + - add many missing functions needed for convenient use. + - **ssd1xxx** + - break dependency from machine package (#812) + - **test** + - Add TestImageRGB888 and TestImageRGB555 + +- **bugfixes** + - **quadrature** + - add RP2350 to quadrature_interrupt.go + - **pixel** + - correct logic error in image size checks in pixel's tests + - correct logic error in image size checks in pixel's tests (Monochrome) + - correct RGB555 to RGBA conversion logic + + 0.33.0 --- - **new devices** diff --git a/README.md b/README.md index 5d4f0fd..eba41d7 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PkgGoDev](https://pkg.go.dev/badge/tinygo.org/x/drivers)](https://pkg.go.dev/tinygo.org/x/drivers) [![Build](https://github.com/tinygo-org/drivers/actions/workflows/build.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/drivers/actions/workflows/build.yml) -This package provides a collection of over 100 different hardware drivers for devices such as sensors, displays, wireless adaptors, and actuators, that can be used together with [TinyGo](https://tinygo.org). +This package provides a collection of over 130 different hardware drivers for devices such as sensors, displays, wireless adaptors, and actuators, that can be used together with [TinyGo](https://tinygo.org). For the complete list, please see: https://tinygo.org/docs/reference/devices/ diff --git a/version.go b/version.go index 6a85412..d8ed5c8 100644 --- a/version.go +++ b/version.go @@ -2,4 +2,4 @@ package drivers // Version returns a user-readable string showing the version of the drivers package for support purposes. // Update this value before release of new version of software. -const Version = "0.33.0" +const Version = "0.34.0" From 5d96a566030c14bd32e6dc4b1b4964f16790d681 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Mon, 15 Dec 2025 18:46:26 +0100 Subject: [PATCH 108/137] build: use the latest TinyGo release container instead of the dev container for builds. Signed-off-by: deadprogram --- .github/workflows/build.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c0c4e7f..e3fa5c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,13 +11,12 @@ on: jobs: build: runs-on: ubuntu-latest - container: ghcr.io/tinygo-org/tinygo-dev:latest + container: + image: ghcr.io/tinygo-org/tinygo:latest + options: --user root steps: - - name: Work around CVE-2022-24765 - # We're not on a multi-user machine, so this is safe. - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: TinyGo version check run: tinygo version - name: Enforce Go Formatted Code @@ -25,4 +24,6 @@ jobs: - name: Run unit tests run: make unit-test - name: Run build and smoke tests - run: make smoke-test + run: | + go env -w GOFLAGS=-buildvcs=false + make smoke-test From 2a42fa7cbb66dca915dde35aa1ac1e1ba026ce00 Mon Sep 17 00:00:00 2001 From: Yurii Soldak Date: Sun, 21 Dec 2025 23:21:39 +0100 Subject: [PATCH 109/137] st7735: remove dependency on the machine package --- st7735/st7735.go | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/st7735/st7735.go b/st7735/st7735.go index 6f15781..a763215 100644 --- a/st7735/st7735.go +++ b/st7735/st7735.go @@ -5,12 +5,13 @@ package st7735 // import "tinygo.org/x/drivers/st7735" import ( "image/color" - "machine" "time" "errors" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/internal/pin" "tinygo.org/x/drivers/pixel" ) @@ -39,10 +40,10 @@ type Device = DeviceOf[pixel.RGB565BE] // formats. type DeviceOf[T Color] struct { bus drivers.SPI - dcPin machine.Pin - resetPin machine.Pin - csPin machine.Pin - blPin machine.Pin + dcPin pin.OutputFunc + resetPin pin.OutputFunc + csPin pin.OutputFunc + blPin pin.OutputFunc width int16 height int16 columnOffset int16 @@ -65,23 +66,25 @@ type Config struct { } // New creates a new ST7735 connection. The SPI wire must already be configured. -func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device { +func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin pin.Output) Device { return NewOf[pixel.RGB565BE](bus, resetPin, dcPin, csPin, blPin) } // NewOf creates a new ST7735 connection with a particular pixel format. The SPI // wire must already be configured. -func NewOf[T Color](bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) DeviceOf[T] { - dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - blPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) +func NewOf[T Color](bus drivers.SPI, resetPin, dcPin, csPin, blPin pin.Output) DeviceOf[T] { + // IMPORTANT: pin configuration should really be done outside of this driver, + // but for backwards compatibility with existing code, we do it here. + legacy.ConfigurePinOut(dcPin) + legacy.ConfigurePinOut(resetPin) + legacy.ConfigurePinOut(csPin) + legacy.ConfigurePinOut(blPin) return DeviceOf[T]{ bus: bus, - dcPin: dcPin, - resetPin: resetPin, - csPin: csPin, - blPin: blPin, + dcPin: dcPin.Set, + resetPin: resetPin.Set, + csPin: csPin.Set, + blPin: blPin.Set, } } @@ -423,7 +426,7 @@ func (d *DeviceOf[T]) Data(data uint8) { // Tx sends data to the display func (d *DeviceOf[T]) Tx(data []byte, isCommand bool) { - d.dcPin.Set(!isCommand) + d.dcPin(!isCommand) d.bus.Tx(data, nil) } From a35786be704b95fd7fe92c58c89c1af1bd3704ba Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 10 Dec 2025 19:47:24 +0100 Subject: [PATCH 110/137] sx127x: add functions used for FSK radio communication Signed-off-by: deadprogram --- smoketest.sh | 1 + sx127x/registers.go | 6 ++++++ sx127x/sx127x.go | 49 +++++++++++++++++++++++++++++++++++---------- 3 files changed, 45 insertions(+), 11 deletions(-) diff --git a/smoketest.sh b/smoketest.sh index aee7229..d25133d 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -124,6 +124,7 @@ tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/xpt2046/mai tinygo build -size short -o ./build/test.elf -target=m5stack-core2 ./examples/ft6336/basic/ tinygo build -size short -o ./build/test.elf -target=m5stack-core2 ./examples/ft6336/touchpaint/ tinygo build -size short -o ./build/test.hex -target=nucleo-wl55jc ./examples/sx126x/lora_rxtx/ +tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/sx127x/lora_rxtx/ tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/ssd1289/main.go tinygo build -size short -o ./build/test.hex -target=pico ./examples/irremote/main.go tinygo build -size short -o ./build/test.hex -target=badger2040 ./examples/uc8151/main.go diff --git a/sx127x/registers.go b/sx127x/registers.go index 903eb51..9dd7379 100644 --- a/sx127x/registers.go +++ b/sx127x/registers.go @@ -96,6 +96,12 @@ const ( SX127X_OPMODE_RX_SINGLE = uint8(0x06) SX127X_OPMODE_CAD = uint8(0x07) + SX127X_OPMODE_LOW_FREQUENCY = uint8(0x4) + + SX127X_OPMODE_MODULATION_MASK = uint8(0x60) + SX127X_OPMODE_MODULATION_FSK = uint8(0x0) + SX127X_OPMODE_MODULATION_OOK = uint8(0x20) + SX127X_LORA_MAC_PUBLIC_SYNCWORD = 0x34 SX127X_LORA_MAC_PRIVATE_SYNCWORD = 0x14 ) diff --git a/sx127x/sx127x.go b/sx127x/sx127x.go index f2c2a87..8b23929 100644 --- a/sx127x/sx127x.go +++ b/sx127x/sx127x.go @@ -25,9 +25,9 @@ type Device struct { rstPin machine.Pin // GPIO for reset radioEventChan chan lora.RadioEvent // Channel for Receiving events loraConf lora.Config // Current Lora configuration - controller RadioController // to manage interactions with the radio + controller RadioController // to manage interrupts with the radio deepSleep bool // Internal Sleep state - deviceType int // sx1261,sx1262,sx1268 (defaults sx1261) + deviceType int // sx1272, sx1273, sx1276, sx1279 (defaults sx1276) spiTxBuf []byte // global Tx buffer to avoid heap allocations in interrupt spiRxBuf []byte // global Rx buffer to avoid heap allocations in interrupt } @@ -65,6 +65,11 @@ func (d *Device) SetRadioController(rc RadioController) error { return nil } +// Specify device type (sx1272, sx1273, sx1276, sx1279) +func (d *Device) SetDeviceType(devType int) { + d.deviceType = devType +} + // Reset re-initialize the sx127x device func (d *Device) Reset() { d.rstPin.Low() @@ -81,9 +86,11 @@ func (d *Device) DetectDevice() bool { // ReadRegister reads register value func (d *Device) ReadRegister(reg uint8) uint8 { - d.controller.SetNss(false) + if d.controller != nil { + d.controller.SetNss(false) + } + // Send register - //d.spiTxBuf = []byte{reg & 0x7f} d.spiTxBuf = d.spiTxBuf[:0] d.spiTxBuf = append(d.spiTxBuf, byte(reg&0x7f)) d.spi.Tx(d.spiTxBuf, nil) @@ -91,13 +98,19 @@ func (d *Device) ReadRegister(reg uint8) uint8 { d.spiRxBuf = d.spiRxBuf[:0] d.spiRxBuf = append(d.spiRxBuf, 0) d.spi.Tx(nil, d.spiRxBuf) - d.controller.SetNss(true) + if d.controller != nil { + d.controller.SetNss(true) + } + return d.spiRxBuf[0] } // WriteRegister writes value to register func (d *Device) WriteRegister(reg uint8, value uint8) uint8 { - d.controller.SetNss(false) + if d.controller != nil { + d.controller.SetNss(false) + } + // Send register d.spiTxBuf = d.spiTxBuf[:0] d.spiTxBuf = append(d.spiTxBuf, byte(reg|0x80)) @@ -108,7 +121,10 @@ func (d *Device) WriteRegister(reg uint8, value uint8) uint8 { d.spiRxBuf = d.spiRxBuf[:0] d.spiRxBuf = append(d.spiRxBuf, 0) d.spi.Tx(d.spiTxBuf, d.spiRxBuf) - d.controller.SetNss(true) + if d.controller != nil { + d.controller.SetNss(true) + } + return d.spiRxBuf[0] } @@ -119,9 +135,20 @@ func (d *Device) SetOpMode(mode uint8) { d.WriteRegister(SX127X_REG_OP_MODE, new) } -// SetOpMode changes the sx1276 mode +// SetOpModeLora changes the sx1276 mode to lora. func (d *Device) SetOpModeLora() { - d.WriteRegister(SX127X_REG_OP_MODE, SX127X_OPMODE_LORA) + d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)|SX127X_OPMODE_LORA) +} + +// SetOpModeFsk changes the sx1276 mode to fsk/ook. +func (d *Device) SetOpModeFsk() { + d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)&^SX127X_OPMODE_LORA) +} + +// SetModulationType changes the modulation type (SX127X_OPMODE_MODULATION_FSK, SX127X_OPMODE_MODULATION_OOK) +func (d *Device) SetModulationType(typ uint8) { + cleared := d.ReadRegister(SX127X_REG_OP_MODE) &^ SX127X_OPMODE_MODULATION_MASK + d.WriteRegister(SX127X_REG_OP_MODE, cleared|typ) } // GetVersion returns hardware version of sx1276 chipset @@ -244,9 +271,9 @@ func (d *Device) SetLowDataRateOptim(val uint8) { // SetLowFrequencyModeOn enables Low Data Rate Optimization func (d *Device) SetLowFrequencyModeOn(val bool) { if val { - d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)|0x04) + d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)|SX127X_OPMODE_LOW_FREQUENCY) } else { - d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)&0xfb) + d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)&^SX127X_OPMODE_LOW_FREQUENCY) } } From c21cd39813be68dc97c9fddc6d89708dc435cb59 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 6 Jan 2026 10:25:09 +0100 Subject: [PATCH 111/137] si5351: complete refactor for more complete interface This completely refactors the interface and implementation for the si5351 clock generator. The interface based on the Arduino implementation was both somewhat hard to work with and also missing a number of important features that are needed to use this chip for RF communication. Instead this new implementation draws inspiration from the efforts of the Traquino community mostly using the rp2040 processor. The TinyGo implementation is based on the patterns and code in the drivers repo for other i2c devices. It also includes some basic unit tests which are not comprehensive but at least provide some coverage. Signed-off-by: deadprogram --- examples/si5351/main.go | 98 +-- si5351/registers.go | 211 ++++-- si5351/si5351.go | 1552 +++++++++++++++++++++++++-------------- si5351/si5351_test.go | 214 ++++++ 4 files changed, 1388 insertions(+), 687 deletions(-) create mode 100644 si5351/si5351_test.go diff --git a/examples/si5351/main.go b/examples/si5351/main.go index d7480a0..4a47754 100644 --- a/examples/si5351/main.go +++ b/examples/si5351/main.go @@ -29,92 +29,60 @@ func main() { // Create driver instance clockgen := si5351.New(machine.I2C0) - // Verify device wired properly - connected, err := clockgen.Connected() - if err != nil { - println("Unable to read device status") - time.Sleep(time.Second) - } - if !connected { - for { - println("Unable to detect si5351 device") - time.Sleep(time.Second) - } + // Initialize device + cnf := si5351.Config{ + Capacitance: si5351.CrystalLoad10PF, } - // Initialise device - clockgen.Configure() + if err := clockgen.Configure(cnf); err != nil { + println("Failed to configure Si5351:", err.Error()) + return + } + println("Si5351 configured") - // Now configue the PLLs and clock outputs. - // The PLLs can be configured with a multiplier and division of the on-board - // 25mhz reference crystal. For example configure PLL A to 900mhz by multiplying - // by 36. This uses an integer multiplier which is more accurate over time - // but allows less of a range of frequencies compared to a fractional - // multiplier shown next. - clockgen.ConfigurePLL(si5351.PLL_A, 36, 0, 1) // Multiply 25mhz by 36 - println("PLL A frequency: 900mhz") - - // And next configure PLL B to 616.6667mhz by multiplying 25mhz by 24.667 using - // the fractional multiplier configuration. Notice you specify the integer - // multiplier and then a numerator and denominator as separate values, i.e. - // numerator 2 and denominator 3 means 2/3 or 0.667. This fractional - // configuration is susceptible to some jitter over time but can set a larger - // range of frequencies. - clockgen.ConfigurePLL(si5351.PLL_B, 24, 2, 3) // Multiply 25mhz by 24.667 (24 2/3) - println("PLL B frequency: 616.6667mhz") - - // Now configure the clock outputs. Each is driven by a PLL frequency as input - // and then further divides that down to a specific frequency. - // Configure clock 0 output to be driven by PLL A divided by 8, so an output - // of 112.5mhz (900mhz / 8). Again this uses the most precise integer division - // but can't set as wide a range of values. - clockgen.ConfigureMultisynth(0, si5351.PLL_A, 8, 0, 1) // Divide by 8 (8 0/1) + // Now configure the clock outputs. + clockgen.SetFrequency(si5351.Clock0, 112_500_000) println("Clock 0: 112.5mhz") - // Next configure clock 1 to be driven by PLL B divided by 45.5 to get - // 13.5531mhz (616.6667mhz / 45.5). This uses fractional division and again - // notice the numerator and denominator are explicitly specified. This is less - // precise but allows a large range of frequencies. - clockgen.ConfigureMultisynth(1, si5351.PLL_B, 45, 1, 2) // Divide by 45.5 (45 1/2) + // Next configure clock 1 for 13.5531mhz (616.6667mhz / 45.5). + // This uses fractional division. + clockgen.SetFrequency(si5351.Clock1, 13_553_125) println("Clock 1: 13.5531mhz") - // Finally configure clock 2 to be driven by PLL B divided once by 900 to get - // down to 685.15 khz and then further divided by a special R divider that - // divides 685.15 khz by 64 to get a final output of 10.706khz. - clockgen.ConfigureMultisynth(2, si5351.PLL_B, 900, 0, 1) // Divide by 900 (900 0/1) - // Set the R divider, this can be a value of: - // - R_DIV_1: divider of 1 - // - R_DIV_2: divider of 2 - // - R_DIV_4: divider of 4 - // - R_DIV_8: divider of 8 - // - R_DIV_16: divider of 16 - // - R_DIV_32: divider of 32 - // - R_DIV_64: divider of 64 - // - R_DIV_128: divider of 128 - clockgen.ConfigureRdiv(2, si5351.R_DIV_64) + // Finally configure clock 2 to output of 10.706khz. + clockgen.SetFrequency(si5351.Clock2, 10_706) println("Clock 2: 10.706khz") - // After configuring PLLs and clocks, enable the outputs. - clockgen.EnableOutputs() + // After configuring the clocks enable the outputs. + clockgen.EnableOutput(si5351.Clock0, true) + clockgen.EnableOutput(si5351.Clock1, true) + clockgen.EnableOutput(si5351.Clock2, true) + println("All outputs enabled") time.Sleep(time.Second) - clockgen.DisableOutputs() + clockgen.EnableOutput(si5351.Clock0, false) + clockgen.EnableOutput(si5351.Clock1, false) + clockgen.EnableOutput(si5351.Clock2, false) println("All outputs disabled for 5 seconds") time.Sleep(5 * time.Second) - // Now use SetFrequency to re-set the frequencies of the outputs + // Now turn clock outputs on and off repeatedly on := false for { if on { - println("Setting Clock 0 output off") - clockgen.OutputEnable(0, false) + println("Setting clock outputs off") + clockgen.EnableOutput(si5351.Clock0, false) + clockgen.EnableOutput(si5351.Clock1, false) + clockgen.EnableOutput(si5351.Clock2, false) on = false } else { - println("Setting Clock 0 output to 100mhz") - clockgen.SetFrequency(100*machine.MHz, 0, si5351.PLL_A) + println("Setting clock outputs on") + clockgen.EnableOutput(si5351.Clock0, true) + clockgen.EnableOutput(si5351.Clock1, true) + clockgen.EnableOutput(si5351.Clock2, true) on = true } - time.Sleep(5 * time.Second) + time.Sleep(1 * time.Second) } } diff --git a/si5351/registers.go b/si5351/registers.go index 1cb8a49..8241202 100644 --- a/si5351/registers.go +++ b/si5351/registers.go @@ -5,75 +5,154 @@ const AddressDefault = 0x60 // Assumes ADDR pin is low const AddressAlternative = 0x61 // Assumes ADDR pin is high const ( - OUTPUT_ENABLE_CONTROL = 3 + XTAL_FREQ = 25000000 + PLL_FIXED = 80000000000 + FREQ_MULT = 100 + DEFAULT_CLK = 1000000000 - CLK0_CONTROL = 16 - CLK1_CONTROL = 17 - CLK2_CONTROL = 18 - CLK3_CONTROL = 19 - CLK4_CONTROL = 20 - CLK5_CONTROL = 21 - CLK6_CONTROL = 22 - CLK7_CONTROL = 23 + PLL_VCO_MIN = 600000000 + PLL_VCO_MAX = 900000000 + MULTISYNTH_MIN_FREQ = 500000 + MULTISYNTH_DIVBY4_FREQ = 150000000 + MULTISYNTH_MAX_FREQ = 225000000 + MULTISYNTH_SHARE_MAX = 100000000 + MULTISYNTH_SHARE_MIN = 1024000 + MULTISYNTH67_MAX_FREQ = MULTISYNTH_DIVBY4_FREQ + CLKOUT_MIN_FREQ = 4000 + CLKOUT_MAX_FREQ = MULTISYNTH_MAX_FREQ + CLKOUT67_MS_MIN = PLL_VCO_MIN / MULTISYNTH67_A_MAX + CLKOUT67_MIN_FREQ = CLKOUT67_MS_MIN / 128 + CLKOUT67_MAX_FREQ = MULTISYNTH67_MAX_FREQ - MULTISYNTH0_PARAMETERS_1 = 42 - MULTISYNTH0_PARAMETERS_3 = 44 - MULTISYNTH1_PARAMETERS_1 = 50 - MULTISYNTH1_PARAMETERS_3 = 52 - MULTISYNTH2_PARAMETERS_1 = 58 - MULTISYNTH2_PARAMETERS_3 = 60 + PLL_A_MIN = 15 + PLL_A_MAX = 90 + PLL_B_MAX = PLL_C_MAX - 1 + PLL_C_MAX = 1048575 + MULTISYNTH_A_MIN = 6 + MULTISYNTH_A_MAX = 1800 + MULTISYNTH67_A_MAX = 254 + MULTISYNTH_B_MAX = MULTISYNTH_C_MAX - 1 + MULTISYNTH_C_MAX = 1048575 + MULTISYNTH_P1_MAX = (1 << 18) - 1 + MULTISYNTH_P2_MAX = (1 << 20) - 1 + MULTISYNTH_P3_MAX = (1 << 20) - 1 + VCXO_PULL_MIN = 30 + VCXO_PULL_MAX = 240 + VCXO_MARGIN = 103 - SPREAD_SPECTRUM_PARAMETERS = 149 + DEVICE_STATUS = 0 + INTERRUPT_STATUS = 1 + INTERRUPT_MASK = 2 + STATUS_SYS_INIT = 1 << 7 + STATUS_LOL_B = 1 << 6 + STATUS_LOL_A = 1 << 5 + STATUS_LOS = 1 << 4 + OUTPUT_ENABLE_CTRL = 3 + OEB_PIN_ENABLE_CTRL = 9 + PLL_INPUT_SOURCE = 15 + CLKIN_DIV_MASK = 3 << 6 + CLKIN_DIV_1 = 0 << 6 + CLKIN_DIV_2 = 1 << 6 + CLKIN_DIV_4 = 2 << 6 + CLKIN_DIV_8 = 3 << 6 + PLLB_SOURCE = 1 << 3 + PLLA_SOURCE = 1 << 2 - PLL_RESET = 177 + CLK0_CTRL = 16 + CLK1_CTRL = 17 + CLK2_CTRL = 18 + CLK3_CTRL = 19 + CLK4_CTRL = 20 + CLK5_CTRL = 21 + CLK6_CTRL = 22 + CLK7_CTRL = 23 + CLK_POWERDOWN = 1 << 7 + CLK_INTEGER_MODE = 1 << 6 + CLK_PLL_SELECT = 1 << 5 + CLK_INVERT = 1 << 4 + CLK_INPUT_MASK = 3 << 2 + CLK_INPUT_XTAL = 0 << 2 + CLK_INPUT_CLKIN = 1 << 2 + CLK_INPUT_MULTISYNTH_0_4 = 2 << 2 + CLK_INPUT_MULTISYNTH_N = 3 << 2 + CLK_DRIVE_STRENGTH_MASK = 3 << 0 + CLK_DRIVE_STRENGTH_2MA = 0 << 0 + CLK_DRIVE_STRENGTH_4MA = 1 << 0 + CLK_DRIVE_STRENGTH_6MA = 2 << 0 + CLK_DRIVE_STRENGTH_8MA = 3 << 0 - CRYSTAL_INTERNAL_LOAD_CAPACITANCE = 183 -) - -const ( - CRYSTAL_LOAD_6PF = (1 << 6) - CRYSTAL_LOAD_8PF = (2 << 6) - CRYSTAL_LOAD_10PF = (3 << 6) -) - -const ( - CRYSTAL_FREQ_25MHZ = 25000000 - CRYSTAL_FREQ_27MHZ = 27000000 -) - -const ( - PLL_A = iota - PLL_B -) - -const ( - R_DIV_1 = iota - R_DIV_2 - R_DIV_4 - R_DIV_8 - R_DIV_16 - R_DIV_32 - R_DIV_64 - R_DIV_128 -) - -const ( - MULTISYNTH_DIV_4 = 4 - MULTISYNTH_DIV_6 = 6 - MULTISYNTH_DIV_8 = 8 -) - -// Frequency constants (in Hz) -const ( - CLKOUT_MIN_FREQ = 8000 // 8 kHz - CLKOUT_MAX_FREQ = 150000000 // 150 MHz - MULTISYNTH_MAX_FREQ = 150000000 // 150 MHz - MULTISYNTH_SHARE_MAX = 100000000 // 100 MHz - MULTISYNTH_DIVBY4_FREQ = 150000000 // 150 MHz - PLL_VCO_MIN = 600000000 // 600 MHz - PLL_VCO_MAX = 900000000 // 900 MHz -) - -const ( - SI5351_PLL_C_MAX = 1048575 + CLK3_0_DISABLE_STATE = 24 + CLK7_4_DISABLE_STATE = 25 + CLK_DISABLE_STATE_MASK = 3 + CLK_DISABLE_STATE_LOW = 0 + CLK_DISABLE_STATE_HIGH = 1 + CLK_DISABLE_STATE_FLOAT = 2 + CLK_DISABLE_STATE_NEVER = 3 + + PARAMETERS_LENGTH = 8 + PLLA_PARAMETERS = 26 + PLLB_PARAMETERS = 34 + CLK0_PARAMETERS = 42 + CLK1_PARAMETERS = 50 + CLK2_PARAMETERS = 58 + CLK3_PARAMETERS = 66 + CLK4_PARAMETERS = 74 + CLK5_PARAMETERS = 82 + CLK6_PARAMETERS = 90 + CLK7_PARAMETERS = 91 + CLK6_7_OUTPUT_DIVIDER = 92 + OUTPUT_CLK_DIV_MASK = 7 << 4 + OUTPUT_CLK6_DIV_MASK = 7 << 0 + OUTPUT_CLK_DIV_SHIFT = 4 + OUTPUT_CLK_DIV6_SHIFT = 0 + OUTPUT_CLK_DIV_1 = 0 + OUTPUT_CLK_DIV_2 = 1 + OUTPUT_CLK_DIV_4 = 2 + OUTPUT_CLK_DIV_8 = 3 + OUTPUT_CLK_DIV_16 = 4 + OUTPUT_CLK_DIV_32 = 5 + OUTPUT_CLK_DIV_64 = 6 + OUTPUT_CLK_DIV_128 = 7 + OUTPUT_CLK_DIVBY4 = 3 << 2 + + SSC_PARAM0 = 149 + SSC_PARAM1 = 150 + SSC_PARAM2 = 151 + SSC_PARAM3 = 152 + SSC_PARAM4 = 153 + SSC_PARAM5 = 154 + SSC_PARAM6 = 155 + SSC_PARAM7 = 156 + SSC_PARAM8 = 157 + SSC_PARAM9 = 158 + SSC_PARAM10 = 159 + SSC_PARAM11 = 160 + SSC_PARAM12 = 161 + + VXCO_PARAMETERS_LOW = 162 + VXCO_PARAMETERS_MID = 163 + VXCO_PARAMETERS_HIGH = 164 + + CLK0_PHASE_OFFSET = 165 + CLK1_PHASE_OFFSET = 166 + CLK2_PHASE_OFFSET = 167 + CLK3_PHASE_OFFSET = 168 + CLK4_PHASE_OFFSET = 169 + CLK5_PHASE_OFFSET = 170 + + PLL_RESET = 177 + PLL_RESET_B = 1 << 7 + PLL_RESET_A = 1 << 5 + + CRYSTAL_LOAD = 183 + CRYSTAL_LOAD_MASK = 3 << 6 + CRYSTAL_LOAD_0PF = 0 << 6 + CRYSTAL_LOAD_6PF = 1 << 6 + CRYSTAL_LOAD_8PF = 2 << 6 + CRYSTAL_LOAD_10PF = 3 << 6 + + FANOUT_ENABLE = 187 + CLKIN_ENABLE = 1 << 7 + XTAL_ENABLE = 1 << 6 + MULTISYNTH_ENABLE = 1 << 4 ) diff --git a/si5351/si5351.go b/si5351/si5351.go index 55c8f2d..3edecd7 100644 --- a/si5351/si5351.go +++ b/si5351/si5351.go @@ -3,8 +3,7 @@ package si5351 import ( "encoding/binary" "errors" - "fmt" - "math" + "time" "tinygo.org/x/drivers" "tinygo.org/x/drivers/internal/regmap" @@ -15,615 +14,1056 @@ type Device struct { bus drivers.I2C Address uint8 - rw regmap.Device8I2C - initialised bool - crystalFreq uint32 - crystalLoad uint8 - pllaConfigured bool - pllaFreq uint32 - pllbConfigured bool - pllbFreq uint32 - lastRdivValue [3]uint8 + rw regmap.Device8I2C + initialized bool + crystalFreq [2]CrystalFrequency + pllaRefOsc PLLReferenceOscillator + pllbRefOsc PLLReferenceOscillator + clkinDiv uint8 + pllaFreq Frequency + pllbFreq Frequency + pllAssignment [8]PLLType + clkFreq [8]Frequency + clkFirstSet [8]bool + refCorrection [2]int32 } -var ErrNotInitialised = errors.New("Si5351 not initialised") -var ErrInvalidParameter = errors.New("Si5351 invalid parameter") +var ( + ErrInitTimeout = errors.New("si5351: init timeout") + ErrNotInitialized = errors.New("si5351: not initialized") + ErrInvalidParameter = errors.New("si5351: invalid parameter") + ErrDeviceNotFound = errors.New("si5351: device not found") + ErrInvalidPLLClockSetting = errors.New("si5351: cannot set >100MHz with other >100MHz on same PLL") + ErrInvalidPLLDivision = errors.New("si5351: CLK6/7 requires integer division ratio") +) -// New creates a new SI5351 connection. The I2C bus must already be configured. -// -// This function only creates the Device object, it does not touch the device. -func New(bus drivers.I2C) Device { - rw := regmap.Device8I2C{} - rw.SetBus(bus, AddressDefault, binary.BigEndian) +// Frequency in Hz +type Frequency uint64 - return Device{ - bus: bus, - rw: rw, - Address: AddressDefault, - crystalFreq: CRYSTAL_FREQ_25MHZ, - crystalLoad: CRYSTAL_LOAD_10PF, - } -} +// CrystalFrequency in Hz +type CrystalFrequency uint32 -// Configure sets up the device for communication -// TODO error handling -func (d *Device) Configure() error { - // // Disable all outputs setting CLKx_DIS high - d.rw.Write8(OUTPUT_ENABLE_CONTROL, 0xFF) +// CrystalLoad options +type CrystalLoad uint8 - // Set the load capacitance for the XTAL - d.rw.Write8(CRYSTAL_INTERNAL_LOAD_CAPACITANCE, d.crystalLoad) +const ( + CrystalLoad0PF CrystalLoad = iota + CrystalLoad6PF + CrystalLoad8PF + CrystalLoad10PF +) - // Power down all output drivers - buf := []byte{CLK0_CONTROL, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80} - d.bus.Tx(uint16(d.Address), buf, nil) +// PLL identifiers +type PLLType uint8 - // Disable spread spectrum output. - if err := d.DisableSpreadSpectrum(); err != nil { - return err - } +const ( + PLL_A PLLType = iota + PLL_B +) - d.initialised = true +// Reference oscillator identifiers +type PLLReferenceOscillator uint8 - return nil -} +const ( + PLLInputXO PLLReferenceOscillator = iota + PLLInputClockIn +) -// Connected returns whether a device at SI5351 address has been found. -func (d *Device) Connected() (bool, error) { - if err := d.bus.Tx(uint16(d.Address), []byte{}, []byte{0}); err != nil { - return false, err - } - return true, nil -} +// Clock output identifiers +type Clock uint8 -// EnableSpreadSpectrum enables spread spectrum modulation to reduce EMI. -func (d *Device) EnableSpreadSpectrum() error { - data, err := d.rw.Read8(SPREAD_SPECTRUM_PARAMETERS) - if err != nil { - return err - } +const ( + Clock0 Clock = iota + Clock1 + Clock2 + Clock3 + Clock4 + Clock5 + Clock6 + Clock7 +) - data |= 0x80 - return d.rw.Write8(SPREAD_SPECTRUM_PARAMETERS, data) -} +const rfracDenominator = Frequency(PLL_C_MAX) -func (d *Device) DisableSpreadSpectrum() error { - data, err := d.rw.Read8(SPREAD_SPECTRUM_PARAMETERS) - if err != nil { - return err - } - - data &^= 0x80 - return d.rw.Write8(SPREAD_SPECTRUM_PARAMETERS, data) -} - -func (d *Device) OutputEnable(output uint8, enable bool) error { - if !d.initialised { - return ErrNotInitialised - } - - // Read the current value of the OUTPUT_ENABLE_CONTROL register - regVal, err := d.rw.Read8(OUTPUT_ENABLE_CONTROL) - if err != nil { - return err - } - - // Modify regVal based on clk and enable - if enable { - regVal &= ^(1 << output) - } else { - regVal |= (1 << output) - } - - // Write the modified value back to the OUTPUT_ENABLE_CONTROL register - return d.rw.Write8(OUTPUT_ENABLE_CONTROL, regVal) -} - -func (d *Device) EnableOutputs() error { - if !d.initialised { - return ErrNotInitialised - } - - return d.rw.Write8(OUTPUT_ENABLE_CONTROL, 0x00) -} - -func (d *Device) DisableOutputs() error { - if !d.initialised { - return ErrNotInitialised - } - return d.rw.Write8(OUTPUT_ENABLE_CONTROL, 0xFF) -} - -// packRegSet packs P1, P2, P3 values into the 8-byte register format -// used by both PLL and Multisynth configuration. -// For multisynth, rDivBits should contain the R divider value shifted left by 4. -// For PLL, rDivBits should be 0. -func packRegSet(p1, p2, p3 uint32, rDivBits uint8) [8]byte { - var data [8]byte - data[0] = uint8((p3 & 0xFF00) >> 8) - data[1] = uint8(p3 & 0xFF) - data[2] = uint8((p1&0x30000)>>16) | rDivBits - data[3] = uint8((p1 & 0xFF00) >> 8) - data[4] = uint8(p1 & 0xFF) - data[5] = uint8(((p3 & 0xF0000) >> 12) | ((p2 & 0xF0000) >> 16)) - data[6] = uint8((p2 & 0xFF00) >> 8) - data[7] = uint8(p2 & 0xFF) - return data -} - -// ConfigurePLL sets the multiplier for the specified PLL -// pll The PLL to configure, which must be one of the following: -// - PLL_A -// - PLL_B -// -// mult The PLL integer multiplier (must be between 15 and 90) -// -// num The 20-bit numerator for fractional output (0..1,048,575). -// Set this to '0' for integer output. -// -// denom The 20-bit denominator for fractional output (1..1,048,575). -// Set this to '1' or higher to avoid divider by zero errors. -// -// PLL Configuration -// fVCO is the PLL output, and must be between 600..900MHz, where: -// -// fVCO = fXTAL * (a+(b/c)) -// -// fXTAL = the crystal input frequency -// a = an integer between 15 and 90 -// b = the fractional numerator (0..1,048,575) -// c = the fractional denominator (1..1,048,575) -// -// NOTE: Try to use integers whenever possible to avoid clock jitter -// (only use the a part, setting b to '0' and c to '1'). -// -// See: http://www.silabs.com/Support%20Documents/TechnicalDocs/AN619.pdf -func (d *Device) ConfigurePLL(pll uint8, mult uint8, num uint32, denom uint32) error { - // Basic validation - switch { - case !d.initialised: - return ErrNotInitialised - // mult = 15..90 - case !((mult > 14) && (mult < 91)): - return ErrInvalidParameter - // Avoid divide by zero - case !(denom > 0): - return ErrInvalidParameter - // 20-bit limit - case !(num <= 0xFFFFF): - return ErrInvalidParameter - // 20-bit limit - case !(denom <= 0xFFFFF): - return ErrInvalidParameter - } - - // Calculate PLL register values - var p1, p2, p3 uint32 - if num == 0 { - // Integer mode - p1 = 128*uint32(mult) - 512 - p2 = num - p3 = denom - } else { - // Fractional mode - p1 = uint32(128*float64(mult) + math.Floor(128*(float64(num)/float64(denom))) - 512) - p2 = uint32(128*float64(num) - float64(denom)*math.Floor(128*(float64(num)/float64(denom)))) - p3 = denom - } - - // Get the appropriate starting point for the PLL registers - baseaddr := uint8(26) - if pll == PLL_B { - baseaddr = 34 - } - - // Pack and write registers - data := packRegSet(p1, p2, p3, 0) - if err := d.bus.Tx(uint16(baseaddr), data[:], nil); err != nil { - return err - } - - // Reset both PLLs - if err := d.rw.Write8(PLL_RESET, (1<<7)|(1<<5)); err != nil { - return err - } - - // Store the frequency settings for use with the Multisynth helper - fvco := float64(d.crystalFreq) * (float64(mult) + (float64(num) / float64(denom))) - if pll == PLL_A { - d.pllaConfigured = true - d.pllaFreq = uint32(math.Floor(fvco)) - } else { - d.pllbConfigured = true - d.pllbFreq = uint32(math.Floor(fvco)) - } - return nil -} - -// ConfigureMultisynth divider, which determines the -// output clock frequency based on the specified PLL input. -// -// output The output channel to use (0..2) -// -// pll The PLL input source to use, which must be one of: -// - PLL_A -// - PLL_B -// -// div The integer divider for the Multisynth output. -// -// If pure integer values are used, this value must be one of: -// - MULTISYNTH_DIV_4 -// - MULTISYNTH_DIV_6 -// - MULTISYNTH_DIV_8 -// If fractional output is used, this value must be between 8 and 900. -// -// num The 20-bit numerator for fractional output (0..1,048,575). -// -// Set this to '0' for integer output. -// -// denom The 20-bit denominator for fractional output (1..1,048,575). -// -// Set this to '1' or higher to avoid divide by zero errors. -// -// # Output Clock Configuration -// -// The multisynth dividers are applied to the specified PLL output, -// and are used to reduce the PLL output to a valid range (500kHz -// to 160MHz). The relationship can be seen in this formula, where -// fVCO is the PLL output frequency and MSx is the multisynth divider: -// -// fOUT = fVCO / MSx -// -// Valid multisynth dividers are 4, 6, or 8 when using integers, -// or any fractional values between 8 + 1/1,048,575 and 900 + 0/1 -// The following formula is used for the fractional mode divider: -// -// a + b / c -// -// a = The integer value, which must be 4, 6 or 8 in integer mode (MSx_INT=1) or 8..900 in fractional mode (MSx_INT=0). -// b = The fractional numerator (0..1,048,575) -// c = The fractional denominator (1..1,048,575) -// -// NOTE: Try to use integers whenever possible to avoid clock jitter -// NOTE: For output frequencies > 150MHz, you must set the divider -// -// to 4 and adjust to PLL to generate the frequency (for example -// a PLL of 640 to generate a 160MHz output clock). This is not -// yet supported in the driver, which limits frequencies to 500kHz .. 150MHz. -// -// NOTE: For frequencies below 500kHz (down to 8kHz) Rx_DIV must be -// -// used, but this isn't currently implemented in the driver. -func (d *Device) ConfigureMultisynth(output uint8, pll uint8, div uint32, num uint32, denom uint32) error { - // Basic validation - switch { - case !d.initialised: - return ErrNotInitialised - // Channel range - case !(output < 3): - return fmt.Errorf("output channel must be between 0 and 2") - // Divider integer value - case !((div > 3) && (div < 2049)): - return ErrInvalidParameter - // Avoid divide by zero - case !(denom > 0): - return ErrInvalidParameter - // 20-bit limit - case !(num <= 0xFFFFF): - return ErrInvalidParameter - // 20-bit limit - case !(denom <= 0xFFFFF): - return ErrInvalidParameter - // Make sure the requested PLL has been initialised - case pll == PLL_A && !d.pllaConfigured: - return ErrInvalidParameter - case pll == PLL_B && !d.pllbConfigured: - return ErrInvalidParameter - } - - // Calculate register values - var reg si5351RegSet - switch { - case num == 0: - // Integer mode - reg.p1 = 128*div - 512 - reg.p2 = 0 - reg.p3 = denom - case denom == 1: - // Fractional mode, simplified calculations - reg.p1 = 128*div + 128*num - 512 - reg.p2 = 128*num - 128 - reg.p3 = 1 - default: - // Fractional mode - reg.p1 = uint32(128*float64(div) + math.Floor(128*(float64(num)/float64(denom))) - 512) - reg.p2 = uint32(128*float64(num) - float64(denom)*math.Floor(128*(float64(num)/float64(denom)))) - reg.p3 = denom - } - - // Determine if we should use integer mode - intMode := num == 0 - - // Use existing R divider value (0 if not previously set) - rDiv := d.lastRdivValue[output] >> 4 - - return d.setMS(output, reg, intMode, rDiv, pll) -} - -func (d *Device) ConfigureRdiv(output uint8, div uint8) error { - // Channel range - if !(output < 3) { - return ErrInvalidParameter - } - - var register uint8 - switch output { - case 0: - register = MULTISYNTH0_PARAMETERS_3 - case 1: - register = MULTISYNTH1_PARAMETERS_3 - case 2: - register = MULTISYNTH2_PARAMETERS_3 - } - - data, err := d.rw.Read8(register) - if err != nil { - return err - } - - d.lastRdivValue[output] = (div & 0x07) << 4 - data = (data & 0x0F) | d.lastRdivValue[output] - return d.rw.Write8(register, data) -} - -// si5351RegSet holds the register values for multisynth configuration -type si5351RegSet struct { +// RegisterSet holds PLL/multisynth register values +type RegisterSet struct { p1 uint32 p2 uint32 p3 uint32 } -var ErrFrequencyOutOfRange = errors.New("Si5351 frequency out of range") -var ErrClockConflict = errors.New("Si5351 clock conflict with existing configuration") +// New creates a new SI5351 connection. The I2C bus must already be configured. +func New(bus drivers.I2C) *Device { + rw := regmap.Device8I2C{} + rw.SetBus(bus, AddressDefault, binary.BigEndian) -// SetFrequency sets the clock frequency of the specified CLK output. -// Frequency range is 8 kHz to 150 MHz. -// -// freq - Output frequency in Hz -// output - Clock output (0, 1, or 2 for this driver) -// pll - The PLL to use (PLL_A or PLL_B) -func (d *Device) SetFrequency(freq uint64, output uint8, pll uint8) error { - switch { - case !d.initialised: - return ErrNotInitialised - case output > 2: - return ErrInvalidParameter + d := Device{ + bus: bus, + rw: rw, + Address: AddressDefault, + pllaRefOsc: PLLInputXO, + pllbRefOsc: PLLInputXO, + clkinDiv: CLKIN_DIV_1, + } + d.crystalFreq[0] = XTAL_FREQ + + return &d +} + +// Config holds configuration parameters for the SI5351. +type Config struct { + Capacitance CrystalLoad + CrystalOutput CrystalFrequency + Correction int32 +} + +// Configure initializes the SI5351 with the specified crystal load capacitance, +// reference oscillator frequency, and frequency correction. +func (d *Device) Configure(cfg Config) error { + // Check for device on bus + if err := d.bus.Tx(uint16(d.Address), []byte{}, []byte{0}); err != nil { + return ErrDeviceNotFound } - switch { - // Lower bounds check - case freq < CLKOUT_MIN_FREQ: - freq = CLKOUT_MIN_FREQ - // Upper bounds check - case freq > MULTISYNTH_MAX_FREQ: - freq = MULTISYNTH_MAX_FREQ + // Wait for SYS_INIT flag to clear + timeout := time.Now().Add(100 * time.Millisecond) + for { + status, err := d.rw.Read8(DEVICE_STATUS) + if err != nil { + return err + } + if (status >> 7) == 0 { + break + } + if time.Now().After(timeout) { + return ErrInitTimeout + } + time.Sleep(time.Millisecond) } - // Select the proper R divider value for low frequencies - rDiv := d.selectRDiv(&freq) - - // Calculate PLL and multisynth parameters - var pllFreq uint64 - switch { - case pll == PLL_A && d.pllaConfigured: - pllFreq = uint64(d.pllaFreq) - case pll == PLL_B && d.pllbConfigured: - pllFreq = uint64(d.pllbFreq) + // Set crystal load capacitance + var xtalLoadC uint8 + switch cfg.Capacitance { + case CrystalLoad0PF: + xtalLoadC = CRYSTAL_LOAD_0PF + case CrystalLoad6PF: + xtalLoadC = CRYSTAL_LOAD_6PF + case CrystalLoad8PF: + xtalLoadC = CRYSTAL_LOAD_8PF + case CrystalLoad10PF: + xtalLoadC = CRYSTAL_LOAD_10PF default: - // PLL not configured, calculate optimal PLL frequency - pllFreq = d.calculatePLLFreq(freq) + xtalLoadC = CRYSTAL_LOAD_10PF + } + if err := d.rw.Write8(CRYSTAL_LOAD, uint8(xtalLoadC&CRYSTAL_LOAD_MASK)|0x12); err != nil { + return err } - // Calculate multisynth divider parameters - msReg := d.multisynthCalc(freq, pllFreq) + // Set up the XO reference frequency + if cfg.CrystalOutput == 0 { + cfg.CrystalOutput = XTAL_FREQ + } + d.SetReferenceFrequency(PLLInputXO, cfg.CrystalOutput) - // Determine if we should use integer mode - intMode := msReg.p2 == 0 + // Set frequency calibration for XO + if err := d.SetCorrection(PLLInputXO, cfg.Correction); err != nil { + return err + } - // Configure PLL if not already configured or if we need a new frequency - if (pll == PLL_A && !d.pllaConfigured) || (pll == PLL_B && !d.pllbConfigured) { - if err := d.setPLL(pllFreq, pll); err != nil { + // Reset device + if err := d.Reset(); err != nil { + return err + } + + d.initialized = true + return nil +} + +// Reset resets the Si5351. +func (d *Device) Reset() error { + // Power down all outputs + for i := range uint8(8) { + if err := d.rw.Write8(CLK0_CTRL+i, 0x80); err != nil { return err } } - // Set multisynth registers - if err := d.setMS(output, msReg, intMode, rDiv, pll); err != nil { + time.Sleep(100 * time.Millisecond) + + // Turn clocks back on with default settings + for i := range uint8(8) { + if err := d.rw.Write8(CLK0_CTRL+i, 0x0C); err != nil { + return err + } + } + + time.Sleep(100 * time.Millisecond) + + // Set PLLA and PLLB to 800 MHz + if err := d.SetPLL(PLL_A, PLL_FIXED); err != nil { + return err + } + if err := d.SetPLL(PLL_B, PLL_FIXED); err != nil { return err } - // Enable output - return d.OutputEnable(output, true) -} + // Make PLL to CLK assignments + for i := range 6 { + d.pllAssignment[i] = PLL_A + d.SetMultisynthSource(Clock(i), PLL_A) + } + d.pllAssignment[6] = PLL_B + d.pllAssignment[7] = PLL_B + d.SetMultisynthSource(Clock(6), PLL_B) + d.SetMultisynthSource(Clock(7), PLL_B) -// selectRDiv selects the appropriate R divider for low frequencies -// and modifies the frequency accordingly -func (d *Device) selectRDiv(freq *uint64) uint8 { - var rDiv uint8 = 0 + // Reset VCXO parameters + d.rw.Write8(VXCO_PARAMETERS_LOW, 0) + d.rw.Write8(VXCO_PARAMETERS_MID, 0) + d.rw.Write8(VXCO_PARAMETERS_HIGH, 0) - if *freq >= CLKOUT_MIN_FREQ && *freq < CLKOUT_MIN_FREQ*2 { - rDiv = R_DIV_128 - *freq *= 128 - } else if *freq >= CLKOUT_MIN_FREQ*2 && *freq < CLKOUT_MIN_FREQ*4 { - rDiv = R_DIV_64 - *freq *= 64 - } else if *freq >= CLKOUT_MIN_FREQ*4 && *freq < CLKOUT_MIN_FREQ*8 { - rDiv = R_DIV_32 - *freq *= 32 - } else if *freq >= CLKOUT_MIN_FREQ*8 && *freq < CLKOUT_MIN_FREQ*16 { - rDiv = R_DIV_16 - *freq *= 16 - } else if *freq >= CLKOUT_MIN_FREQ*16 && *freq < CLKOUT_MIN_FREQ*32 { - rDiv = R_DIV_8 - *freq *= 8 - } else if *freq >= CLKOUT_MIN_FREQ*32 && *freq < CLKOUT_MIN_FREQ*64 { - rDiv = R_DIV_4 - *freq *= 4 - } else if *freq >= CLKOUT_MIN_FREQ*64 && *freq < CLKOUT_MIN_FREQ*128 { - rDiv = R_DIV_2 - *freq *= 2 + // Reset PLLs + d.PLLReset(PLL_A) + d.PLLReset(PLL_B) + + // Initialize clock state + for i := range 8 { + d.clkFreq[i] = 0 + d.EnableOutput(Clock(i), false) + d.clkFirstSet[i] = false } - return rDiv + return nil } -// calculatePLLFreq calculates an optimal PLL frequency for the given output frequency -func (d *Device) calculatePLLFreq(freq uint64) uint64 { - // Try to find an integer divider that puts PLL in valid range (600-900 MHz) - // Start with a divider that gives us a PLL freq near 750 MHz (middle of range) - targetPLL := uint64(750000000) - divider := targetPLL / freq +// SetPLL programs the specified PLL with the given frequency. +func (d *Device) SetPLL(pll PLLType, pllFreq Frequency) error { + var refOsc PLLReferenceOscillator + var baseAddr uint8 - // Ensure divider is in valid range (8-900 for fractional, 4/6/8 for integer) - - switch { - case divider < 8: - divider = 8 - case divider > 900: - divider = 900 - } - - pllFreq := freq * divider - - // Ensure PLL frequency is in valid range - switch { - case pllFreq < PLL_VCO_MIN: - pllFreq = PLL_VCO_MIN - case pllFreq > PLL_VCO_MAX: - pllFreq = PLL_VCO_MAX - } - - return pllFreq -} - -// multisynthCalc calculates the multisynth register values -func (d *Device) multisynthCalc(freq, pllFreq uint64) si5351RegSet { - var reg si5351RegSet - - // Calculate the division ratio - // divider = pllFreq / freq - a := uint32(pllFreq / freq) - remainder := pllFreq % freq - - // Calculate b and c for fractional part - // We use c = SI5351_PLL_C_MAX (max 20-bit value) for best resolution - c := uint32(SI5351_PLL_C_MAX) - b := uint32((uint64(remainder) * uint64(c)) / freq) - - // Calculate P1, P2, P3 - // P1 = 128 * a + floor(128 * b / c) - 512 - // P2 = 128 * b - c * floor(128 * b / c) - // P3 = c - floor128bc := uint32((128 * uint64(b)) / uint64(c)) - - reg.p1 = 128*a + floor128bc - 512 - reg.p2 = 128*b - c*floor128bc - reg.p3 = c - - return reg -} - -// setPLL configures the PLL with the specified frequency -func (d *Device) setPLL(pllFreq uint64, pll uint8) error { - // Calculate PLL multiplier from crystal frequency - // pllFreq = crystalFreq * (a + b/c) - xtalFreq := uint64(d.crystalFreq) - - a := uint32(pllFreq / xtalFreq) - remainder := pllFreq % xtalFreq - - // Use max denominator for best resolution - c := uint32(SI5351_PLL_C_MAX) - b := uint32((remainder * uint64(c)) / xtalFreq) - - return d.ConfigurePLL(pll, uint8(a), b, c) -} - -// setMS sets the multisynth registers for the specified output -func (d *Device) setMS(output uint8, reg si5351RegSet, intMode bool, rDiv uint8, pll uint8) error { - // Get the appropriate starting point for the registers - var baseaddr uint8 - switch output { - case 0: - baseaddr = MULTISYNTH0_PARAMETERS_1 - case 1: - baseaddr = MULTISYNTH1_PARAMETERS_1 - case 2: - baseaddr = MULTISYNTH2_PARAMETERS_1 + switch pll { + case PLL_A: + refOsc = d.pllaRefOsc + baseAddr = PLLA_PARAMETERS + d.pllaFreq = pllFreq + case PLL_B: + refOsc = d.pllbRefOsc + baseAddr = PLLB_PARAMETERS + d.pllbFreq = pllFreq default: return ErrInvalidParameter } - // Store R divider value - d.lastRdivValue[output] = (rDiv & 0x07) << 4 + _, reg := d.CalculatePLL(pll, pllFreq, d.refCorrection[refOsc], false) - // Pack and write registers - data := packRegSet(reg.p1, reg.p2, reg.p3, d.lastRdivValue[output]) - if err := d.bus.Tx(uint16(baseaddr), data[:], nil); err != nil { + params := make([]byte, 8) + params[0] = byte((reg.p3 >> 8) & 0xFF) + params[1] = byte(reg.p3 & 0xFF) + params[2] = byte((reg.p1 >> 16) & 0x03) + params[3] = byte((reg.p1 >> 8) & 0xFF) + params[4] = byte(reg.p1 & 0xFF) + params[5] = byte(((reg.p3 >> 12) & 0xF0) | ((reg.p2 >> 16) & 0x0F)) + params[6] = byte((reg.p2 >> 8) & 0xFF) + params[7] = byte(reg.p2 & 0xFF) + + for i := range params { + if err := d.rw.Write8(baseAddr+uint8(i), params[i]); err != nil { + return err + } + } + + return nil +} + +// SetFrequency sets the clock frequency of the specified CLK output. +// Frequency range is 8 kHz to 150 MHz for CLK0-5, up to 150 MHz for CLK6-7. +func (d *Device) SetFrequency(clk Clock, freq Frequency) error { + if !d.initialized { + return ErrNotInitialized + } + + freqMult := freq * FREQ_MULT + + switch { + case clk <= 5: + return d.setFreqCLK0to5(clk, freqMult) + case clk <= 7: + return d.setFreqCLK6to7(clk, freqMult) + default: + return ErrInvalidParameter + } +} + +// SetRawFrequency sets the clock frequency of the specified CLK output without +// applying the frequency multiplier. +// Frequency range is 8 kHz to 150 MHz for CLK0-5, up to 150 MHz for CLK6-7. +func (d *Device) SetRawFrequency(clk Clock, freq Frequency) error { + if !d.initialized { + return ErrNotInitialized + } + + switch { + case clk <= 5: + return d.setFreqCLK0to5(clk, freq) + case clk <= 7: + return d.setFreqCLK6to7(clk, freq) + default: + return ErrInvalidParameter + } +} + +// SetMultisynthSource sets the PLL source for a multisynth. +func (d *Device) SetMultisynthSource(clk Clock, pll PLLType) error { + regVal, err := d.rw.Read8(CLK0_CTRL + uint8(clk)) + if err != nil { return err } - // Configure the clk control register - clkControlReg := uint8(0x0F) // 8mA drive strength, powered up - if pll == PLL_B { - clkControlReg |= (1 << 5) // Use PLLB - } - if intMode { - clkControlReg |= (1 << 6) // Integer mode - } - - var clkReg uint8 - switch output { - case 0: - clkReg = CLK0_CONTROL - case 1: - clkReg = CLK1_CONTROL - case 2: - clkReg = CLK2_CONTROL - } - - return d.rw.Write8(clkReg, clkControlReg) -} - -// GetFreqStep returns the frequency step size of the radio in Hz. -// This is the smallest frequency increment that can be achieved, -// determined by the PLL frequency and denominator resolution. -// If pll is PLL_A, uses PLLA settings; if PLL_B, uses PLLB settings. -// Returns 0 if the specified PLL is not configured. -func (d *Device) GetFreqStep(pll uint8) uint64 { - // The frequency step at the output is: - // step = pllFreq / (SI5351_PLL_C_MAX * multisynth_divider) - // - // However, since multisynth divider varies per output, we return - // the base step from the PLL, which is: - // step = pllFreq / SI5351_PLL_C_MAX - - var pllFreq uint64 - switch pll { case PLL_A: - if !d.pllaConfigured { - return 0 - } - pllFreq = uint64(d.pllaFreq) + regVal &^= CLK_PLL_SELECT case PLL_B: - if !d.pllbConfigured { - return 0 - } - pllFreq = uint64(d.pllbFreq) + regVal |= CLK_PLL_SELECT default: - return 0 + return ErrInvalidParameter } - return pllFreq / SI5351_PLL_C_MAX + if err := d.rw.Write8(CLK0_CTRL+uint8(clk), regVal); err != nil { + return err + } + + d.pllAssignment[clk] = pll + return nil +} + +// SetCorrection sets the oscillator correction factor in parts-per-billion. +func (d *Device) SetCorrection(refOsc PLLReferenceOscillator, corr int32) error { + d.refCorrection[refOsc] = corr + + if err := d.SetPLL(PLL_A, d.pllaFreq); err != nil { + return err + } + if err := d.SetPLL(PLL_B, d.pllbFreq); err != nil { + return err + } + return nil +} + +// GetCorrection returns the oscillator correction factor in parts-per-billion. +func (d *Device) GetCorrection(refOsc PLLReferenceOscillator) int32 { + return d.refCorrection[refOsc] +} + +// PLLReset applies a reset to the indicated PLL. +func (d *Device) PLLReset(pll PLLType) error { + switch pll { + case PLL_A: + return d.rw.Write8(PLL_RESET, PLL_RESET_A) + case PLL_B: + return d.rw.Write8(PLL_RESET, PLL_RESET_B) + } + return ErrInvalidParameter +} + +// SetReferenceFrequency sets the reference frequency for the specified reference oscillator. +func (d *Device) SetReferenceFrequency(refOsc PLLReferenceOscillator, refFreq CrystalFrequency) { + switch { + case refFreq <= 30_000_000: + d.crystalFreq[refOsc] = refFreq + if refOsc == PLLInputClockIn { + d.clkinDiv = CLKIN_DIV_1 + } + case refFreq <= 60_000_000: + d.crystalFreq[refOsc] = refFreq / 2 + if refOsc == PLLInputClockIn { + d.clkinDiv = CLKIN_DIV_2 + } + case refFreq <= 100_000_000: + d.crystalFreq[refOsc] = refFreq / 4 + if refOsc == PLLInputClockIn { + d.clkinDiv = CLKIN_DIV_4 + } + } +} + +// EnableOutput enables or disables a clock output. +func (d *Device) EnableOutput(clk Clock, enable bool) error { + if clk > Clock7 { + return ErrInvalidParameter + } + + regVal, err := d.rw.Read8(OUTPUT_ENABLE_CTRL) + if err != nil { + return err + } + + if enable { + regVal &^= (1 << clk) + } else { + regVal |= (1 << clk) + } + + return d.rw.Write8(OUTPUT_ENABLE_CTRL, regVal) +} + +type DriveStrength uint8 + +const ( + DriveStrength2MA DriveStrength = iota + DriveStrength4MA + DriveStrength6MA + DriveStrength8MA +) + +// SetDriveStrength sets the drive strength of the specified clock output. +func (d *Device) SetDriveStrength(clk Clock, drive DriveStrength) error { + if clk > Clock7 { + return ErrInvalidParameter + } + + regVal, err := d.rw.Read8(CLK0_CTRL + uint8(clk)) + if err != nil { + return err + } + + regVal &^= 0x03 + + switch drive { + case DriveStrength2MA: // 2mA + regVal |= CLK_DRIVE_STRENGTH_2MA + case DriveStrength4MA: // 4mA + regVal |= CLK_DRIVE_STRENGTH_4MA + case DriveStrength6MA: // 6mA + regVal |= CLK_DRIVE_STRENGTH_6MA + case DriveStrength8MA: // 8mA + regVal |= CLK_DRIVE_STRENGTH_8MA + default: + return ErrInvalidParameter + } + + return d.rw.Write8(CLK0_CTRL+uint8(clk), regVal) +} + +// SetPhase sets the 7-bit phase register for the specified clock. +func (d *Device) SetPhase(clk Clock, phase uint8) error { + phase &= 0x7F // Mask upper bit + return d.rw.Write8(CLK0_PHASE_OFFSET+uint8(clk), phase) +} + +// Fanout options for clock signals +type Fanout uint8 + +const ( + FanoutClockIn Fanout = iota + FanoutXO + FanoutMultisynth +) + +// SetClockFanout enables or disables the clock fanout options for individual clock outputs. +// If you intend to output the XO or CLKIN on the clock outputs, enable this first. +// By default, only the Multisynth fanout is enabled at startup. +func (d *Device) SetClockFanout(fanout Fanout, enable bool) error { + regVal, err := d.rw.Read8(FANOUT_ENABLE) + if err != nil { + return err + } + + switch fanout { + case FanoutClockIn: + if enable { + regVal |= CLKIN_ENABLE + } else { + regVal &^= CLKIN_ENABLE + } + case FanoutXO: + if enable { + regVal |= XTAL_ENABLE + } else { + regVal &^= XTAL_ENABLE + } + case FanoutMultisynth: + if enable { + regVal |= MULTISYNTH_ENABLE + } else { + regVal &^= MULTISYNTH_ENABLE + } + default: + return ErrInvalidParameter + } + + return d.rw.Write8(FANOUT_ENABLE, regVal) +} + +// Clock source options +type ClockSource uint8 + +const ( + ClockSourceXTAL ClockSource = iota + ClockSourceClockIn + ClockSourceMS0 + ClockSourceMS +) + +// SetClockSource sets the clock source for a multisynth (based on the options +// presented for Registers 16-23 in the Silicon Labs AN619 document). +// Choices are XTAL, CLKIN, MS0, or the multisynth associated with the clock output. +func (d *Device) SetClockSource(clk Clock, src ClockSource) error { + if clk > Clock7 { + return ErrInvalidParameter + } + + regVal, err := d.rw.Read8(CLK0_CTRL + uint8(clk)) + if err != nil { + return err + } + + // Clear the input mask bits first + regVal &^= CLK_INPUT_MASK + + switch src { + case ClockSourceXTAL: + regVal |= CLK_INPUT_XTAL + case ClockSourceClockIn: + regVal |= CLK_INPUT_CLKIN + case ClockSourceMS0: + if clk == Clock0 { + return ErrInvalidParameter + } + regVal |= CLK_INPUT_MULTISYNTH_0_4 + case ClockSourceMS: + regVal |= CLK_INPUT_MULTISYNTH_N + default: + return ErrInvalidParameter + } + + return d.rw.Write8(CLK0_CTRL+uint8(clk), regVal) +} + +// SetClockPower enables or disables power to a clock output (a power saving feature). +func (d *Device) SetClockPower(clk Clock, enable bool) error { + if clk > Clock7 { + return ErrInvalidParameter + } + + regVal, err := d.rw.Read8(CLK0_CTRL + uint8(clk)) + if err != nil { + return err + } + + if enable { + regVal &= 0x7F // Clear bit 7 (power on) + } else { + regVal |= 0x80 // Set bit 7 (power off) + } + + return d.rw.Write8(CLK0_CTRL+uint8(clk), regVal) +} + +// SetClockInvert inverts the clock output waveform. +func (d *Device) SetClockInvert(clk Clock, invert bool) error { + if clk > Clock7 { + return ErrInvalidParameter + } + + regVal, err := d.rw.Read8(CLK0_CTRL + uint8(clk)) + if err != nil { + return err + } + + if invert { + regVal |= CLK_INVERT + } else { + regVal &^= CLK_INVERT + } + + return d.rw.Write8(CLK0_CTRL+uint8(clk), regVal) +} + +// CalculatePLL calculates the PLL register values for the specified frequency +func (d *Device) CalculatePLL(pll PLLType, freq Frequency, correction int32, vcxo bool) (Frequency, RegisterSet) { + var refFreq Frequency + if pll == PLL_A { + refFreq = Frequency(d.crystalFreq[d.pllaRefOsc]) * FREQ_MULT + } else { + refFreq = Frequency(d.crystalFreq[d.pllbRefOsc]) * FREQ_MULT + } + + // Apply correction + refFreq = refFreq + Frequency(((int64(correction)<<31)/1000000000)*int64(refFreq)>>31) + + // Bounds checking + switch { + case freq < PLL_VCO_MIN*FREQ_MULT: + freq = PLL_VCO_MIN * FREQ_MULT + case freq > PLL_VCO_MAX*FREQ_MULT: + freq = PLL_VCO_MAX * FREQ_MULT + } + + a := uint32(freq / refFreq) + + switch { + case a < PLL_A_MIN: + freq = refFreq * PLL_A_MIN + case a > PLL_A_MAX: + freq = refFreq * PLL_A_MAX + } + + var b, c uint32 + if vcxo { + b = uint32(((freq % refFreq) * 1000000) / refFreq) + c = 1000000 + } else { + b = uint32(((freq % refFreq) * rfracDenominator) / refFreq) + if b != 0 { + c = uint32(rfracDenominator) + } else { + c = 1 + } + } + + p1 := 128*a + ((128 * b) / c) - 512 + p2 := 128*b - c*((128*b)/c) + p3 := c + + lltmp := (refFreq * Frequency(b)) / Frequency(c) + freqOut := lltmp + refFreq*Frequency(a) + + reg := RegisterSet{p1: p1, p2: p2, p3: p3} + + if vcxo { + return Frequency(128*a*1000000 + b), reg + } + return freqOut, reg +} + +// CalculateMultisynth calculates the multisynth register values for the specified frequency +func (d *Device) CalculateMultisynth(freq, pllFreq Frequency) (Frequency, RegisterSet) { + divby4 := false + retVal := uint8(0) + + // Bounds checking + switch { + case freq > MULTISYNTH_MAX_FREQ*FREQ_MULT: + freq = MULTISYNTH_MAX_FREQ * FREQ_MULT + case freq < MULTISYNTH_MIN_FREQ*FREQ_MULT: + freq = MULTISYNTH_MIN_FREQ * FREQ_MULT + } + + if freq >= MULTISYNTH_DIVBY4_FREQ*FREQ_MULT { + divby4 = true + } + + var a, b, c uint32 + + if pllFreq == 0 { + if !divby4 { + lltmp := Frequency(PLL_VCO_MAX * FREQ_MULT) + lltmp = lltmp / freq + switch lltmp { + case 5: + lltmp = 4 + case 7: + lltmp = 6 + } + a = uint32(lltmp) + } else { + a = 4 + } + b = 0 + c = 1 + pllFreq = Frequency(a) * freq + } else { + retVal = 1 + a = uint32(pllFreq / freq) + + switch { + case a < MULTISYNTH_A_MIN: + freq = pllFreq / MULTISYNTH_A_MIN + a = MULTISYNTH_A_MIN + case a > MULTISYNTH_A_MAX: + freq = pllFreq / MULTISYNTH_A_MAX + a = MULTISYNTH_A_MAX + } + + b = uint32(((pllFreq % freq) * rfracDenominator) / freq) + if b != 0 { + c = uint32(rfracDenominator) + } else { + c = 1 + } + } + + var p1, p2, p3 uint32 + if divby4 { + p3 = 1 + p2 = 0 + p1 = 0 + } else { + p1 = 128*a + ((128 * b) / c) - 512 + p2 = 128*b - c*((128*b)/c) + p3 = c + } + + reg := RegisterSet{p1: p1, p2: p2, p3: p3} + + if retVal == 0 { + return pllFreq, reg + } + return freq, reg +} + +// SetMultisynth programs the multisynth registers for the specified clock. +// For CLK0-5, reg contains p1, p2, p3 values. For CLK6/7, only p1 is used. +func (d *Device) SetMultisynth(clk Clock, reg RegisterSet, intMode, rDiv, divBy4 uint8) error { + switch { + case clk <= 5: + params := make([]byte, 8) + params[0] = byte((reg.p3 >> 8) & 0xFF) + params[1] = byte(reg.p3 & 0xFF) + + regVal, err := d.rw.Read8(CLK0_PARAMETERS + 2 + uint8(clk)*8) + if err != nil { + return err + } + regVal &^= 0x03 + params[2] = regVal | byte((reg.p1>>16)&0x03) + + params[3] = byte((reg.p1 >> 8) & 0xFF) + params[4] = byte(reg.p1 & 0xFF) + params[5] = byte(((reg.p3 >> 12) & 0xF0) | ((reg.p2 >> 16) & 0x0F)) + params[6] = byte((reg.p2 >> 8) & 0xFF) + params[7] = byte(reg.p2 & 0xFF) + + baseAddr := CLK0_PARAMETERS + uint8(clk)*8 + for i := range params { + if err := d.rw.Write8(baseAddr+uint8(i), params[i]); err != nil { + return err + } + } + + d.setInt(clk, intMode) + return d.msDiv(clk, rDiv, divBy4) + case clk <= 7: + // CLK6/7 + baseAddr := CLK6_PARAMETERS + if clk == 7 { + baseAddr = CLK7_PARAMETERS + } + if err := d.rw.Write8(uint8(baseAddr), byte(reg.p1)); err != nil { + return err + } + return d.msDiv(clk, rDiv, divBy4) + default: + return ErrInvalidParameter + } +} + +func (d *Device) setFreqCLK0to5(clk Clock, freq Frequency) error { + var rDiv uint8 + var divBy4 uint8 + var intMode uint8 + + // Bounds checking + switch { + case freq < CLKOUT_MIN_FREQ*FREQ_MULT: + freq = CLKOUT_MIN_FREQ * FREQ_MULT + case freq > MULTISYNTH_MAX_FREQ*FREQ_MULT: + freq = MULTISYNTH_MAX_FREQ * FREQ_MULT + } + + // Check if frequency requires PLL recalculation + if freq > MULTISYNTH_SHARE_MAX*FREQ_MULT { + // Check other clocks on same PLL + for i := range Clock(6) { + if d.clkFreq[i] > MULTISYNTH_SHARE_MAX*FREQ_MULT { + if i != clk && d.pllAssignment[i] == d.pllAssignment[clk] { + return ErrInvalidPLLClockSetting + } + } + } + + // Enable output on first set + if !d.clkFirstSet[clk] { + d.EnableOutput(clk, true) + d.clkFirstSet[clk] = true + } + + d.clkFreq[clk] = freq + + // Calculate PLL frequency + pllFreq, _ := d.CalculateMultisynth(freq, 0) + d.SetPLL(d.pllAssignment[clk], pllFreq) + + // Recalculate other synths on same PLL + for i := range Clock(6) { + if d.clkFreq[i] != 0 && d.pllAssignment[i] == d.pllAssignment[clk] { + tempFreq := d.clkFreq[i] + tempFreq, rDiv = d.selectRDiv(tempFreq) + + _, tempReg := d.CalculateMultisynth(tempFreq, pllFreq) + + if tempFreq >= MULTISYNTH_DIVBY4_FREQ*FREQ_MULT { + divBy4 = 1 + intMode = 1 + } else { + divBy4 = 0 + intMode = 0 + } + + d.SetMultisynth(i, tempReg, intMode, rDiv, divBy4) + } + } + + d.PLLReset(d.pllAssignment[clk]) + } else { + d.clkFreq[clk] = freq + + if !d.clkFirstSet[clk] { + d.EnableOutput(clk, true) + d.clkFirstSet[clk] = true + } + + freq, rDiv = d.selectRDiv(freq) + + var pllFreq Frequency + if d.pllAssignment[clk] == PLL_A { + pllFreq = d.pllaFreq + } else { + pllFreq = d.pllbFreq + } + + _, msReg := d.CalculateMultisynth(freq, pllFreq) + d.SetMultisynth(clk, msReg, intMode, rDiv, divBy4) + } + + return nil +} + +func (d *Device) setFreqCLK6to7(clk Clock, freq Frequency) error { + var rDiv uint8 + var divBy4 uint8 + var intMode uint8 + + // Bounds checking for CLK6/7 + if freq > 0 && freq < CLKOUT67_MIN_FREQ*FREQ_MULT { + freq = CLKOUT_MIN_FREQ * FREQ_MULT + } + if freq >= MULTISYNTH_DIVBY4_FREQ*FREQ_MULT { + freq = MULTISYNTH_DIVBY4_FREQ*FREQ_MULT - 1 + } + + var msReg RegisterSet + var pllFreq Frequency + + otherClk := uint8(7) + if clk == 7 { + otherClk = 6 + } + + if d.clkFreq[otherClk] != 0 { + // Other CLK6/7 already set, must use integer division + if d.pllbFreq%freq != 0 || (d.pllbFreq/freq)%2 != 0 { + return ErrInvalidPLLDivision + } + + d.clkFreq[clk] = freq + freq, rDiv = d.selectRDivMS67(freq) + _, msReg = d.multisynth67Calc(freq, d.pllbFreq) + } else { + // Set PLLB based on this clock + d.clkFreq[clk] = freq + freq, rDiv = d.selectRDivMS67(freq) + pllFreq, msReg = d.multisynth67Calc(freq, 0) + + d.SetPLL(d.pllAssignment[clk], pllFreq) + } + + divBy4 = 0 + intMode = 0 + + return d.SetMultisynth(clk, msReg, intMode, rDiv, divBy4) +} + +func (d *Device) setInt(clk Clock, enable uint8) error { + regVal, err := d.rw.Read8(CLK0_CTRL + uint8(clk)) + if err != nil { + return err + } + + if enable == 1 { + regVal |= CLK_INTEGER_MODE + } else { + regVal &^= CLK_INTEGER_MODE + } + + return d.rw.Write8(CLK0_CTRL+uint8(clk), regVal) +} + +func (d *Device) msDiv(clk Clock, rDiv, divBy4 uint8) error { + var regAddr uint8 + + switch clk { + case 0: + regAddr = CLK0_PARAMETERS + 2 + case 1: + regAddr = CLK1_PARAMETERS + 2 + case 2: + regAddr = CLK2_PARAMETERS + 2 + case 3: + regAddr = CLK3_PARAMETERS + 2 + case 4: + regAddr = CLK4_PARAMETERS + 2 + case 5: + regAddr = CLK5_PARAMETERS + 2 + case 6, 7: + regAddr = CLK6_7_OUTPUT_DIVIDER + default: + return ErrInvalidParameter + } + + regVal, err := d.rw.Read8(regAddr) + if err != nil { + return err + } + + switch { + case clk <= 5: + regVal &^= 0x7C + + if divBy4 == 0 { + regVal &^= OUTPUT_CLK_DIVBY4 + } else { + regVal |= OUTPUT_CLK_DIVBY4 + } + + regVal |= (rDiv << OUTPUT_CLK_DIV_SHIFT) + case clk == 6: + regVal &^= 0x07 + regVal |= rDiv + case clk == 7: + regVal &^= 0x70 + regVal |= (rDiv << OUTPUT_CLK_DIV_SHIFT) + } + + return d.rw.Write8(regAddr, regVal) +} + +func (d *Device) selectRDiv(freq Frequency) (Frequency, uint8) { + rDiv := OUTPUT_CLK_DIV_1 + + switch { + case freq >= CLKOUT_MIN_FREQ*FREQ_MULT && freq < CLKOUT_MIN_FREQ*FREQ_MULT*2: + rDiv = OUTPUT_CLK_DIV_128 + freq *= 128 + case freq >= CLKOUT_MIN_FREQ*FREQ_MULT*2 && freq < CLKOUT_MIN_FREQ*FREQ_MULT*4: + rDiv = OUTPUT_CLK_DIV_64 + freq *= 64 + case freq >= CLKOUT_MIN_FREQ*FREQ_MULT*4 && freq < CLKOUT_MIN_FREQ*FREQ_MULT*8: + rDiv = OUTPUT_CLK_DIV_32 + freq *= 32 + case freq >= CLKOUT_MIN_FREQ*FREQ_MULT*8 && freq < CLKOUT_MIN_FREQ*FREQ_MULT*16: + rDiv = OUTPUT_CLK_DIV_16 + freq *= 16 + case freq >= CLKOUT_MIN_FREQ*FREQ_MULT*16 && freq < CLKOUT_MIN_FREQ*FREQ_MULT*32: + rDiv = OUTPUT_CLK_DIV_8 + freq *= 8 + case freq >= CLKOUT_MIN_FREQ*FREQ_MULT*32 && freq < CLKOUT_MIN_FREQ*FREQ_MULT*64: + rDiv = OUTPUT_CLK_DIV_4 + freq *= 4 + case freq >= CLKOUT_MIN_FREQ*FREQ_MULT*64 && freq < CLKOUT_MIN_FREQ*FREQ_MULT*128: + rDiv = OUTPUT_CLK_DIV_2 + freq *= 2 + } + + return freq, uint8(rDiv) +} + +func (d *Device) selectRDivMS67(freq Frequency) (Frequency, uint8) { + rDiv := OUTPUT_CLK_DIV_1 + + // The minimum frequency for MS67 with max divider is lower than the calculated constant + // We use the same ranges as selectRDiv for consistency + minFreq := Frequency(CLKOUT_MIN_FREQ * FREQ_MULT) + + switch { + case freq >= minFreq && freq < minFreq*2: + rDiv = OUTPUT_CLK_DIV_128 + freq *= 128 + case freq >= minFreq*2 && freq < minFreq*4: + rDiv = OUTPUT_CLK_DIV_64 + freq *= 64 + case freq >= minFreq*4 && freq < minFreq*8: + rDiv = OUTPUT_CLK_DIV_32 + freq *= 32 + case freq >= minFreq*8 && freq < minFreq*16: + rDiv = OUTPUT_CLK_DIV_16 + freq *= 16 + case freq >= minFreq*16 && freq < minFreq*32: + rDiv = OUTPUT_CLK_DIV_8 + freq *= 8 + case freq >= minFreq*32 && freq < minFreq*64: + rDiv = OUTPUT_CLK_DIV_4 + freq *= 4 + case freq >= minFreq*64 && freq < minFreq*128: + rDiv = OUTPUT_CLK_DIV_2 + freq *= 2 + } + + return freq, uint8(rDiv) +} + +func (d *Device) multisynth67Calc(freq, pllFreq Frequency) (Frequency, RegisterSet) { + // Bounds checking + if freq > MULTISYNTH67_MAX_FREQ*FREQ_MULT { + freq = MULTISYNTH67_MAX_FREQ * FREQ_MULT + } + if freq < MULTISYNTH_MIN_FREQ*FREQ_MULT { + freq = MULTISYNTH_MIN_FREQ * FREQ_MULT + } + + var a uint32 + + if pllFreq == 0 { + lltmp := Frequency(PLL_VCO_MAX*FREQ_MULT - MULTISYNTH_SHARE_MAX) + lltmp = lltmp / freq + a = uint32(lltmp) + + // Must be even + if a%2 != 0 { + a++ + } + + // Bounds check + if a < MULTISYNTH_A_MIN { + a = MULTISYNTH_A_MIN + } + if a > MULTISYNTH67_A_MAX { + a = MULTISYNTH67_A_MAX + } + + pllFreq = Frequency(a) * freq + + // PLL bounds + if pllFreq > PLL_VCO_MAX*FREQ_MULT { + a -= 2 + pllFreq = Frequency(a) * freq + } else if pllFreq < PLL_VCO_MIN*FREQ_MULT { + a += 2 + pllFreq = Frequency(a) * freq + } + + return pllFreq, RegisterSet{p1: a, p2: 0, p3: 0} + } else { + if pllFreq%freq != 0 { + return 0, RegisterSet{} + } + + a = uint32(pllFreq / freq) + + if a < MULTISYNTH_A_MIN || a > MULTISYNTH67_A_MAX { + return 0, RegisterSet{} + } + + return 1, RegisterSet{p1: a, p2: 0, p3: 0} + } } diff --git a/si5351/si5351_test.go b/si5351/si5351_test.go new file mode 100644 index 0000000..392556f --- /dev/null +++ b/si5351/si5351_test.go @@ -0,0 +1,214 @@ +package si5351 + +import ( + "testing" +) + +func TestSelectRDiv(t *testing.T) { + d := &Device{} + + tests := []struct { + name string + freq Frequency + wantDiv uint8 + wantFreq Frequency + }{ + {"4kHz", 4000 * FREQ_MULT, OUTPUT_CLK_DIV_128, 4000 * FREQ_MULT * 128}, + {"8kHz", 8000 * FREQ_MULT, OUTPUT_CLK_DIV_64, 8000 * FREQ_MULT * 64}, + {"16kHz", 16000 * FREQ_MULT, OUTPUT_CLK_DIV_32, 16000 * FREQ_MULT * 32}, + {"32kHz", 32000 * FREQ_MULT, OUTPUT_CLK_DIV_16, 32000 * FREQ_MULT * 16}, + {"64kHz", 64000 * FREQ_MULT, OUTPUT_CLK_DIV_8, 64000 * FREQ_MULT * 8}, + {"128kHz", 128000 * FREQ_MULT, OUTPUT_CLK_DIV_4, 128000 * FREQ_MULT * 4}, + {"256kHz", 256000 * FREQ_MULT, OUTPUT_CLK_DIV_2, 256000 * FREQ_MULT * 2}, + {"512kHz", 512000 * FREQ_MULT, OUTPUT_CLK_DIV_1, 512000 * FREQ_MULT}, + {"1MHz", 1000000 * FREQ_MULT, OUTPUT_CLK_DIV_1, 1000000 * FREQ_MULT}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + freq := tt.freq + freq, gotDiv := d.selectRDiv(freq) + if gotDiv != tt.wantDiv { + t.Errorf("selectRDiv() div = %v, want %v", gotDiv, tt.wantDiv) + } + if freq != tt.wantFreq { + t.Errorf("selectRDiv() freq = %v, want %v", freq, tt.wantFreq) + } + }) + } +} + +func TestSelectRDivMS67(t *testing.T) { + d := &Device{} + + tests := []struct { + name string + freq Frequency + wantDiv uint8 + wantFreq Frequency + }{ + {"4kHz", 4000 * FREQ_MULT, OUTPUT_CLK_DIV_128, 4000 * FREQ_MULT * 128}, + {"8kHz", 8000 * FREQ_MULT, OUTPUT_CLK_DIV_64, 8000 * FREQ_MULT * 64}, + {"16kHz", 16000 * FREQ_MULT, OUTPUT_CLK_DIV_32, 16000 * FREQ_MULT * 32}, + {"64kHz", 64000 * FREQ_MULT, OUTPUT_CLK_DIV_8, 64000 * FREQ_MULT * 8}, + {"256kHz", 256000 * FREQ_MULT, OUTPUT_CLK_DIV_2, 256000 * FREQ_MULT * 2}, + {"512kHz", 512000 * FREQ_MULT, OUTPUT_CLK_DIV_1, 512000 * FREQ_MULT}, + {"1MHz", 1000000 * FREQ_MULT, OUTPUT_CLK_DIV_1, 1000000 * FREQ_MULT}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + freq := tt.freq + freq, gotDiv := d.selectRDivMS67(freq) + if gotDiv != tt.wantDiv { + t.Errorf("selectRDivMS67() div = %v, want %v", gotDiv, tt.wantDiv) + } + if freq != tt.wantFreq { + t.Errorf("selectRDivMS67() freq = %v, want %v", freq, tt.wantFreq) + } + }) + } +} + +func TestCalculatePLL(t *testing.T) { + d := &Device{} + d.crystalFreq[0] = 25000000 + + tests := []struct { + name string + freq Frequency + wantMin Frequency + wantMax Frequency + }{ + {"600MHz", 600000000 * FREQ_MULT, 599000000 * FREQ_MULT, 601000000 * FREQ_MULT}, + {"750MHz", 750000000 * FREQ_MULT, 749000000 * FREQ_MULT, 751000000 * FREQ_MULT}, + {"900MHz", 900000000 * FREQ_MULT, 899000000 * FREQ_MULT, 901000000 * FREQ_MULT}, + {"BelowMin", 500000000 * FREQ_MULT, 600000000 * FREQ_MULT, 600000000 * FREQ_MULT}, + {"AboveMax", 1000000000 * FREQ_MULT, 900000000 * FREQ_MULT, 900000000 * FREQ_MULT}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, reg := d.CalculatePLL(PLL_A, tt.freq, 0, false) + if got < tt.wantMin || got > tt.wantMax { + t.Errorf("CalculatePLL() = %v, want between %v and %v", got, tt.wantMin, tt.wantMax) + } + if reg.p1 == 0 || reg.p3 == 0 { + t.Errorf("CalculatePLL() invalid register values: p1=%v, p2=%v, p3=%v", reg.p1, reg.p2, reg.p3) + } + }) + } +} + +func TestCalculateMultisynth(t *testing.T) { + d := &Device{} + + tests := []struct { + name string + freq Frequency + pllFreq Frequency + wantDiv bool + }{ + {"10MHz from 800MHz", 10000000 * FREQ_MULT, 800000000 * FREQ_MULT, false}, + {"1MHz from 800MHz", 1000000 * FREQ_MULT, 800000000 * FREQ_MULT, false}, + {"Auto PLL 10MHz", 10000000 * FREQ_MULT, 0, false}, + {"150MHz DivBy4", 150000000 * FREQ_MULT, 600000000 * FREQ_MULT, true}, + {"BelowMin", 100000 * FREQ_MULT, 800000000 * FREQ_MULT, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, reg := d.CalculateMultisynth(tt.freq, tt.pllFreq) + if tt.pllFreq == 0 { + // Auto mode should return a valid PLL frequency + if got < PLL_VCO_MIN*FREQ_MULT || got > PLL_VCO_MAX*FREQ_MULT { + t.Errorf("CalculateMultisynth() returned invalid PLL freq %v", got) + } + } + if reg.p3 == 0 { + t.Errorf("CalculateMultisynth() p3 should not be 0") + } + }) + } +} + +func TestMultisynth67Calc(t *testing.T) { + d := &Device{} + + tests := []struct { + name string + freq Frequency + pllFreq Frequency + wantErr bool + }{ + {"10MHz Auto", 10000000 * FREQ_MULT, 0, false}, + {"100MHz Auto", 100000000 * FREQ_MULT, 0, false}, + {"100MHz from 800MHz", 100000000 * FREQ_MULT, 800000000 * FREQ_MULT, false}, + {"Invalid Division", 10000000 * FREQ_MULT, 777000000 * FREQ_MULT, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, reg := d.multisynth67Calc(tt.freq, tt.pllFreq) + if tt.pllFreq == 0 { + if got < PLL_VCO_MIN*FREQ_MULT || got > PLL_VCO_MAX*FREQ_MULT { + t.Errorf("multisynth67Calc() returned invalid PLL freq %v", got) + } + } else if tt.wantErr { + if got != 0 { + t.Errorf("multisynth67Calc() should return 0 for invalid division, got %v", got) + } + } + if reg.p1 == 0 && !tt.wantErr { + t.Errorf("multisynth67Calc() p1 should not be 0") + } + }) + } +} + +func TestSetCorrection(t *testing.T) { + // Skip this test as it requires a mock I2C bus + t.Skip("Requires mock I2C bus implementation") +} + +func TestSetRefFreq(t *testing.T) { + d := &Device{} + + tests := []struct { + name string + freq CrystalFrequency + wantFreq CrystalFrequency + wantDiv uint8 + }{ + {"25MHz", 25000000, 25000000, CLKIN_DIV_1}, + {"50MHz", 50000000, 25000000, CLKIN_DIV_2}, + {"100MHz", 100000000, 25000000, CLKIN_DIV_4}, + {"30MHz", 30000000, 30000000, CLKIN_DIV_1}, + {"60MHz", 60000000, 30000000, CLKIN_DIV_2}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d.SetReferenceFrequency(PLLInputClockIn, tt.freq) + if d.crystalFreq[PLLInputClockIn] != tt.wantFreq { + t.Errorf("SetReferenceFrequency() freq = %v, want %v", d.crystalFreq[PLLInputClockIn], tt.wantFreq) + } + if d.clkinDiv != tt.wantDiv { + t.Errorf("SetReferenceFrequency() clkinDiv = %v, want %v", d.clkinDiv, tt.wantDiv) + } + }) + } +} + +func TestGetCorrection(t *testing.T) { + d := &Device{} + d.refCorrection[PLLInputXO] = 5000 + d.refCorrection[PLLInputClockIn] = -3000 + + if got := d.GetCorrection(PLLInputXO); got != 5000 { + t.Errorf("GetCorrection(PLLInputXO) = %v, want 5000", got) + } + if got := d.GetCorrection(PLLInputClockIn); got != -3000 { + t.Errorf("GetCorrection(PLLInputClockIn) = %v, want -3000", got) + } +} From 0a41786a77ac6f8b12721d4e485f6359d72dba52 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Fri, 26 Dec 2025 19:02:51 +0100 Subject: [PATCH 112/137] gps: improve implementation for UBX config commands Signed-off-by: deadprogram --- gps/gps.go | 2 + gps/ublox.go | 252 +++++++++++++++++++++++++----- gps/ublox_test.go | 380 ++++++++++++++++++++++++++++++++++++++++++++++ gps/ubx.go | 200 ++++++++++++++++++++++++ gps/ubx_test.go | 199 ++++++++++++++++++++++++ 5 files changed, 994 insertions(+), 39 deletions(-) create mode 100644 gps/ublox_test.go create mode 100644 gps/ubx.go create mode 100644 gps/ubx_test.go diff --git a/gps/gps.go b/gps/gps.go index 16ba870..0d196c4 100644 --- a/gps/gps.go +++ b/gps/gps.go @@ -18,6 +18,8 @@ var ( errInvalidGGASentence = errors.New("invalid GGA NMEA sentence") errInvalidRMCSentence = errors.New("invalid RMC NMEA sentence") errInvalidGLLSentence = errors.New("invalid GLL NMEA sentence") + errGPSCommandRejected = errors.New("GPS command rejected (NAK)") + errNoACKToGPSCommand = errors.New("no ACK to GPS command") ) type GPSError struct { diff --git a/gps/ublox.go b/gps/ublox.go index 7d2e00a..c74ceb1 100644 --- a/gps/ublox.go +++ b/gps/ublox.go @@ -1,53 +1,227 @@ package gps import ( - "errors" "time" ) -// flight mode disables the GPS COCOM limits -var flight_mode_cmd = [...]byte{ - 0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0xFF, 0xFF, 0x06, 0x03, 0x00, 0x00, 0x00, - 0x00, 0x10, 0x27, 0x00, 0x00, 0x05, 0x00, 0xFA, 0x00, 0xFA, 0x00, 0x64, 0x00, - 0x2C, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x16, 0xDC} +// FlightModeCmd is a UBX-CFG-NAV5 command to set the GPS into +// flight mode (airborne <1g) +var FlightModeCmd = CfgNav5{ + Mask: CfgNav5Dyn | CfgNav5MinEl | CfgNav5PosFixMode, + DynModel: DynModeAirborne1g, // Airborne with <1g acceleration + FixMode: FixModeAuto, // Auto 2D/3D + MinElev_deg: 5, // Minimum elevation 5 degrees + FixedAlt_me2: 0, // Not used + FixedAltVar_m2e4: 0, // Not used + PDop: 100, // 10.0 + TDop: 100, // 10.0 + PAcc_m: 5000, // 5 meters + TAcc_m: 5000, // 5 meters + StaticHoldThresh_cm_s: 0, // Not used + DgnssTimeout_s: 0, // Not used + CnoThreshNumSVs: 0, // Not used + CnoThresh_dbhz: 0, // Not used + StaticHoldMaxDist_m: 0, // Not used + UtcStandard: 0, // Automatic + Reserved1: [2]byte{}, + Reserved2: [5]byte{}, +} -// Sets CFG-GNSS to disable everything other than GPS GNSS -// solution. Failure to do this means GPS power saving -// doesn't work. Not needed for MAX7, needed for MAX8's -var cfg_gnss_cmd = [...]byte{ - 0xB5, 0x62, 0x06, 0x3E, 0x2C, 0x00, 0x00, 0x00, - 0x20, 0x05, 0x00, 0x08, 0x10, 0x00, 0x01, 0x00, - 0x01, 0x01, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, - 0x01, 0x01, 0x03, 0x08, 0x10, 0x00, 0x00, 0x00, - 0x01, 0x01, 0x05, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x01, 0x01, 0x06, 0x08, 0x0E, 0x00, 0x00, 0x00, - 0x01, 0x01, 0xFC, 0x11} - -func FlightMode(d Device) (err error) { - err = sendCommand(d, flight_mode_cmd[:]) +// SetFlightMode sends UBX-CFG-NAV5 command to set GPS into flight mode +func SetFlightMode(d Device) (err error) { + if _, err = FlightModeCmd.Write(d.buffer[:]); err != nil { + return err + } + err = SendCommand(d, d.buffer[:]) return err } -func SetCfgGNSS(d Device) (err error) { - err = sendCommand(d, cfg_gnss_cmd[:]) - return err -} +var ( + // GGA (time, lat/lng, altitude) + MessageRateGGACmd = CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x00, + Rate: 1, // Every position fix + } + // GLL (time, lat/lng) + MessageRateGLLCmd = CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x01, + Rate: 0, // Disabled + } + // GSA (satellite id list) + MessageRateGSACmd = CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x02, + Rate: 1, // Every position fix + } + // GSV (satellite locations) + MessageRateGSVCmd = CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x03, + Rate: 1, // Every position fix + } + // RMC (time, lat/lng, speed, course) + MessageRateRMCCmd = CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x04, + Rate: 1, // Every position fix + } + // VTG (speed, course) + MessageRateVTGCmd = CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x05, + Rate: 0, // Disabled + } + // ZDA (time, timezone) + MessageRateZDACmd = CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x08, + Rate: 0, // Disabled + } + // TXT (text transmission) + MessageRateTXTCmd = CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x41, + Rate: 0, // Disabled + } +) -func sendCommand(d Device, command []byte) (err error) { - d.WriteBytes(command) - start := time.Now() - for time.Now().Sub(start) < 1000 { - if d.readNextByte() == '\n' { - if d.readNextByte() == 0xB5 { - d.readNextByte() - if d.readNextByte() == 0x05 { - if d.readNextByte() == 0x01 { - return - } - } - } +// SetMessageRatesMinimal configures the GPS to output a minimal set of NMEA sentences +func SetMessageRatesMinimal(d Device) (err error) { + commands := []CfgMsg1{ + MessageRateGSACmd, + MessageRateGGACmd, + MessageRateGLLCmd, + MessageRateGSVCmd, + MessageRateRMCCmd, + MessageRateVTGCmd, + MessageRateZDACmd, + MessageRateTXTCmd, + } + + for _, cmd := range commands { + if _, err = cmd.Write(d.buffer[:]); err != nil { + return err + } + if err = SendCommand(d, d.buffer[:]); err != nil { + return err } } - return errors.New("no ACK to GPS command") + + return nil +} + +// SetMessageRatesAllEnabled configures the GPS to output all NMEA sentences +func SetMessageRatesAllEnabled(d Device) (err error) { + commands := []CfgMsg1{ + MessageRateGSACmd, + MessageRateGGACmd, + MessageRateGLLCmd, + MessageRateGSVCmd, + MessageRateRMCCmd, + MessageRateVTGCmd, + MessageRateZDACmd, + MessageRateTXTCmd, + } + + for _, cmd := range commands { + cmd.Rate = 1 // Enable all messages at 1 Hz + if _, err = cmd.Write(d.buffer[:]); err != nil { + return err + } + if err = SendCommand(d, d.buffer[:]); err != nil { + return err + } + } + + return nil +} + +// GNSSDisableCmd is a UBX-CFG-GNSS command to disable all GNSS but GPS +// Needed for MAX8's, not needed for MAX7 +var GNSSDisableCmd = CfgGnss{ + MsgVer: 0x00, + NumTrkChHw: 0x20, // 32 channels + NumTrkChUse: 0x20, + ConfigBlocks: []*CfgGnssConfigBlocksType{ + {GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Flags: CfgGnssEnable | 0x010000}, // GPS enabled + {GnssId: 1, ResTrkCh: 1, MaxTrkCh: 3, Flags: 0x010000}, // SBAS disabled + {GnssId: 3, ResTrkCh: 8, MaxTrkCh: 16, Flags: 0x010000}, // BeiDou disabled + {GnssId: 5, ResTrkCh: 0, MaxTrkCh: 3, Flags: 0x010000}, // QZSS disabled + {GnssId: 6, ResTrkCh: 8, MaxTrkCh: 14, Flags: 0x010000}, // GLONASS disabled + }, +} + +// SetGNSSDisable sends UBX-CFG-GNSS command to disable all GNSS but GPS +func SetGNSSDisable(d Device) (err error) { + if _, err = GNSSDisableCmd.Write(d.buffer[:]); err != nil { + return err + } + return SendCommand(d, d.buffer[:]) +} + +// SendCommand sends a UBX command and waits for ACK/NAK response +func SendCommand(d Device, command []byte) error { + // Calculate and append checksum + checksummed := appendChecksum(command) + d.WriteBytes(checksummed) + + start := time.Now() + for time.Since(start) < time.Second { + // Look for UBX sync sequence + if d.readNextByte() != ubxSyncChar1 { + continue + } + if d.readNextByte() != ubxSyncChar2 { + continue + } + + // Read message class and ID + msgClass := d.readNextByte() + msgID := d.readNextByte() + + // Check if it's an ACK class message + if msgClass != ubxClassACK { + continue + } + + // Read length (2 bytes, little-endian) - ACK is always 2 bytes payload + lenLo := d.readNextByte() + lenHi := d.readNextByte() + length := uint16(lenLo) | uint16(lenHi)<<8 + + if length != 2 { + continue + } + + // Read ACK payload: class and ID of acknowledged message + ackClass := d.readNextByte() + ackID := d.readNextByte() + + // Verify ACK is for our command (command[2] = class, command[3] = ID) + if ackClass != command[2] || ackID != command[3] { + continue + } + + if msgID == ubxACK_ACK { + return nil + } + if msgID == ubxACK_NAK { + return errGPSCommandRejected + } + } + + return errNoACKToGPSCommand +} + +// appendChecksum calculates UBX checksum and appends it to the message +func appendChecksum(msg []byte) []byte { + var ckA, ckB byte + // Checksum covers class, ID, length, and payload (skip sync chars) + for i := 2; i < len(msg); i++ { + ckA += msg[i] + ckB += ckA + } + return append(msg, ckA, ckB) } diff --git a/gps/ublox_test.go b/gps/ublox_test.go new file mode 100644 index 0000000..73648d6 --- /dev/null +++ b/gps/ublox_test.go @@ -0,0 +1,380 @@ +package gps + +import ( + "testing" +) + +func TestAppendChecksum(t *testing.T) { + testCases := []struct { + name string + input []byte + expected []byte + }{ + { + name: "simple message", + input: []byte{0xB5, 0x62, 0x06, 0x24, 0x00, 0x00}, + expected: []byte{0xB5, 0x62, 0x06, 0x24, 0x00, 0x00, 0x2A, 0x84}, + }, + { + name: "CFG-NAV5 header only", + input: []byte{0xB5, 0x62, 0x06, 0x24, 0x24, 0x00}, + expected: []byte{0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0x4E, 0xCC}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := appendChecksum(tc.input) + + if len(result) != len(tc.expected) { + t.Errorf("expected length %d, got %d", len(tc.expected), len(result)) + return + } + + // Check checksum bytes (last two bytes) + ckA := result[len(result)-2] + ckB := result[len(result)-1] + expectedCkA := tc.expected[len(tc.expected)-2] + expectedCkB := tc.expected[len(tc.expected)-1] + + if ckA != expectedCkA || ckB != expectedCkB { + t.Errorf("expected checksum 0x%02X 0x%02X, got 0x%02X 0x%02X", + expectedCkA, expectedCkB, ckA, ckB) + } + }) + } +} + +func TestAppendChecksumPreservesOriginal(t *testing.T) { + input := []byte{0xB5, 0x62, 0x06, 0x24, 0x00, 0x00} + original := make([]byte, len(input)) + copy(original, input) + + result := appendChecksum(input) + + // Verify original bytes are preserved + for i := range input { + if result[i] != original[i] { + t.Errorf("byte %d changed: expected 0x%02X, got 0x%02X", i, original[i], result[i]) + } + } + + // Verify two bytes were appended + if len(result) != len(input)+2 { + t.Errorf("expected length %d, got %d", len(input)+2, len(result)) + } +} + +func TestFlightModeCmdConfig(t *testing.T) { + // Verify FlightModeCmd has expected values + if FlightModeCmd.DynModel != 6 { + t.Errorf("expected DynModel 6 (airborne <1g), got %d", FlightModeCmd.DynModel) + } + + if FlightModeCmd.FixMode != 3 { + t.Errorf("expected FixMode 3 (auto 2D/3D), got %d", FlightModeCmd.FixMode) + } + + expectedMask := CfgNav5Dyn | CfgNav5MinEl | CfgNav5PosFixMode + if FlightModeCmd.Mask != expectedMask { + t.Errorf("expected Mask 0x%04X, got 0x%04X", expectedMask, FlightModeCmd.Mask) + } + + if FlightModeCmd.MinElev_deg != 5 { + t.Errorf("expected MinElev_deg 5, got %d", FlightModeCmd.MinElev_deg) + } +} + +func TestGNSSDisableCmdConfig(t *testing.T) { + // Verify GNSSDisableCmd has expected structure + if GNSSDisableCmd.MsgVer != 0 { + t.Errorf("expected MsgVer 0, got %d", GNSSDisableCmd.MsgVer) + } + + if GNSSDisableCmd.NumTrkChHw != 0x20 { + t.Errorf("expected NumTrkChHw 0x20, got 0x%02X", GNSSDisableCmd.NumTrkChHw) + } + + if len(GNSSDisableCmd.ConfigBlocks) != 5 { + t.Errorf("expected 5 config blocks, got %d", len(GNSSDisableCmd.ConfigBlocks)) + return + } + + // Verify GPS is enabled + gpsBlock := GNSSDisableCmd.ConfigBlocks[0] + if gpsBlock.GnssId != 0 { + t.Errorf("expected first block GnssId 0 (GPS), got %d", gpsBlock.GnssId) + } + if gpsBlock.Flags&CfgGnssEnable == 0 { + t.Error("expected GPS to be enabled") + } + + // Verify other GNSS are disabled + for i := 1; i < len(GNSSDisableCmd.ConfigBlocks); i++ { + block := GNSSDisableCmd.ConfigBlocks[i] + if block.Flags&CfgGnssEnable != 0 { + t.Errorf("expected block %d (GnssId %d) to be disabled", i, block.GnssId) + } + } +} + +func TestFlightModeCmdWrite(t *testing.T) { + buf := make([]byte, 64) + n, err := FlightModeCmd.Write(buf) + + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if n != 42 { + t.Errorf("expected 42 bytes, got %d", n) + } + + // Verify sync chars + if buf[0] != 0xB5 || buf[1] != 0x62 { + t.Errorf("expected sync 0xB5 0x62, got 0x%02X 0x%02X", buf[0], buf[1]) + } + + // Verify class/id + if buf[2] != 0x06 || buf[3] != 0x24 { + t.Errorf("expected class/id 0x06 0x24, got 0x%02X 0x%02X", buf[2], buf[3]) + } + + // Verify DynModel at offset 8 + if buf[8] != 6 { + t.Errorf("expected DynModel 6, got %d", buf[8]) + } +} + +func TestGNSSDisableCmdWrite(t *testing.T) { + buf := make([]byte, 64) + n, err := GNSSDisableCmd.Write(buf) + + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + // 6 header + 4 payload header + 5*8 blocks = 50 bytes + expectedLen := 6 + 4 + 5*8 + if n != expectedLen { + t.Errorf("expected %d bytes, got %d", expectedLen, n) + } + + // Verify sync chars + if buf[0] != 0xB5 || buf[1] != 0x62 { + t.Errorf("expected sync 0xB5 0x62, got 0x%02X 0x%02X", buf[0], buf[1]) + } + + // Verify class/id + if buf[2] != 0x06 || buf[3] != 0x3E { + t.Errorf("expected class/id 0x06 0x3E, got 0x%02X 0x%02X", buf[2], buf[3]) + } + + // Verify number of blocks + if buf[9] != 5 { + t.Errorf("expected 5 blocks, got %d", buf[9]) + } +} + +func TestChecksumCalculation(t *testing.T) { + // Test with known UBX message and expected checksum + // This is a minimal CFG-NAV5 poll message + msg := []byte{0xB5, 0x62, 0x06, 0x24, 0x00, 0x00} + + result := appendChecksum(msg) + + // Verify checksum by recalculating + var ckA, ckB byte + for i := 2; i < len(msg); i++ { + ckA += msg[i] + ckB += ckA + } + + if result[6] != ckA || result[7] != ckB { + t.Errorf("checksum mismatch: expected 0x%02X 0x%02X, got 0x%02X 0x%02X", + ckA, ckB, result[6], result[7]) + } +} + +func TestMessageRateCmdConfigs(t *testing.T) { + testCases := []struct { + name string + cmd CfgMsg1 + msgClass byte + msgID byte + rate byte + }{ + {"GGA", MessageRateGGACmd, 0xF0, 0x00, 1}, + {"GLL", MessageRateGLLCmd, 0xF0, 0x01, 0}, + {"GSA", MessageRateGSACmd, 0xF0, 0x02, 1}, + {"GSV", MessageRateGSVCmd, 0xF0, 0x03, 1}, + {"RMC", MessageRateRMCCmd, 0xF0, 0x04, 1}, + {"VTG", MessageRateVTGCmd, 0xF0, 0x05, 0}, + {"ZDA", MessageRateZDACmd, 0xF0, 0x08, 0}, + {"TXT", MessageRateTXTCmd, 0xF0, 0x41, 0}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.cmd.MsgClass != tc.msgClass { + t.Errorf("expected MsgClass 0x%02X, got 0x%02X", tc.msgClass, tc.cmd.MsgClass) + } + if tc.cmd.MsgID != tc.msgID { + t.Errorf("expected MsgID 0x%02X, got 0x%02X", tc.msgID, tc.cmd.MsgID) + } + if tc.cmd.Rate != tc.rate { + t.Errorf("expected Rate %d, got %d", tc.rate, tc.cmd.Rate) + } + }) + } +} + +func TestCfgMsg1Write(t *testing.T) { + cmd := CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x00, + Rate: 1, + } + + buf := make([]byte, 16) + n, err := cmd.Write(buf) + + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if n != 9 { + t.Errorf("expected 9 bytes, got %d", n) + } + + // Verify sync chars + if buf[0] != 0xB5 || buf[1] != 0x62 { + t.Errorf("expected sync 0xB5 0x62, got 0x%02X 0x%02X", buf[0], buf[1]) + } + + // Verify class/id (0x06 0x01 for CFG-MSG) + if buf[2] != 0x06 || buf[3] != 0x01 { + t.Errorf("expected class/id 0x06 0x01, got 0x%02X 0x%02X", buf[2], buf[3]) + } + + // Verify length (3 bytes payload) + if buf[4] != 3 || buf[5] != 0 { + t.Errorf("expected length 3, got %d", uint16(buf[4])|uint16(buf[5])<<8) + } + + // Verify payload + if buf[6] != 0xF0 { + t.Errorf("expected MsgClass 0xF0, got 0x%02X", buf[6]) + } + if buf[7] != 0x00 { + t.Errorf("expected MsgID 0x00, got 0x%02X", buf[7]) + } + if buf[8] != 1 { + t.Errorf("expected Rate 1, got %d", buf[8]) + } +} + +func TestCfgMsg1ClassID(t *testing.T) { + cmd := CfgMsg1{} + if got := cmd.classID(); got != 0x0106 { + t.Errorf("expected 0x0106, got 0x%04x", got) + } +} + +func TestMinimalMessageRatesConfig(t *testing.T) { + // Verify the minimal config has correct rates set + // GGA and RMC should be enabled (rate=1), others disabled (rate=0) + expectedRates := map[byte]byte{ + 0x00: 1, // GGA - enabled + 0x01: 0, // GLL - disabled + 0x02: 1, // GSA - enabled + 0x03: 1, // GSV - enabled + 0x04: 1, // RMC - enabled + 0x05: 0, // VTG - disabled + 0x08: 0, // ZDA - disabled + 0x41: 0, // TXT - disabled + } + + commands := []CfgMsg1{ + MessageRateGGACmd, + MessageRateGLLCmd, + MessageRateGSACmd, + MessageRateGSVCmd, + MessageRateRMCCmd, + MessageRateVTGCmd, + MessageRateZDACmd, + MessageRateTXTCmd, + } + + for _, cmd := range commands { + expectedRate, ok := expectedRates[cmd.MsgID] + if !ok { + t.Errorf("unexpected MsgID 0x%02X", cmd.MsgID) + continue + } + if cmd.Rate != expectedRate { + t.Errorf("MsgID 0x%02X: expected rate %d, got %d", cmd.MsgID, expectedRate, cmd.Rate) + } + } +} + +func TestAllMessageRatesWriteCorrectBytes(t *testing.T) { + // Test that each message rate command writes the correct bytes + commands := []CfgMsg1{ + MessageRateGGACmd, + MessageRateGLLCmd, + MessageRateGSACmd, + MessageRateGSVCmd, + MessageRateRMCCmd, + MessageRateVTGCmd, + MessageRateZDACmd, + MessageRateTXTCmd, + } + + for _, cmd := range commands { + buf := make([]byte, 16) + n, err := cmd.Write(buf) + + if err != nil { + t.Errorf("MsgID 0x%02X: unexpected error: %v", cmd.MsgID, err) + continue + } + + if n != 9 { + t.Errorf("MsgID 0x%02X: expected 9 bytes, got %d", cmd.MsgID, n) + } + + // Verify MsgClass in payload + if buf[6] != 0xF0 { + t.Errorf("MsgID 0x%02X: expected MsgClass 0xF0, got 0x%02X", cmd.MsgID, buf[6]) + } + + // Verify MsgID in payload + if buf[7] != cmd.MsgID { + t.Errorf("expected MsgID 0x%02X in payload, got 0x%02X", cmd.MsgID, buf[7]) + } + + // Verify Rate in payload + if buf[8] != cmd.Rate { + t.Errorf("MsgID 0x%02X: expected Rate %d, got %d", cmd.MsgID, cmd.Rate, buf[8]) + } + } +} + +func TestSetMessageRatesAllEnabledModifiesRate(t *testing.T) { + // Verify that when we copy a command and set Rate=1, it works correctly + cmd := MessageRateGLLCmd // This one is disabled by default + if cmd.Rate != 0 { + t.Errorf("expected GLL default rate 0, got %d", cmd.Rate) + } + + // Simulate what SetMessageRatesAllEnabled does + cmd.Rate = 1 + + buf := make([]byte, 16) + _, _ = cmd.Write(buf) + + if buf[8] != 1 { + t.Errorf("expected Rate 1 in buffer, got %d", buf[8]) + } +} diff --git a/gps/ubx.go b/gps/ubx.go new file mode 100644 index 0000000..b84625f --- /dev/null +++ b/gps/ubx.go @@ -0,0 +1,200 @@ +package gps + +// UBX message classes +const ( + ubxClassACK = 0x05 +) + +// UBX ACK message IDs +const ( + ubxACK_NAK = 0x00 // Message not acknowledged + ubxACK_ACK = 0x01 // Message acknowledged +) + +// UBX sync characters +const ( + ubxSyncChar1 = 0xB5 + ubxSyncChar2 = 0x62 +) + +const ( + DynModePortable = 0 + DynModeStationary = 2 + DynModePedestrian = 3 + DynModeAutomotive = 4 + DynModeSea = 5 + DynModeAirborne1g = 6 + DynModeAirborne2g = 7 + DynModeAirborne4g = 8 + DynModeWristWatch = 9 + DynModeBike = 10 +) + +const ( + FixMode2D = 1 + FixMode3D = 2 + FixModeAuto = 3 +) + +// from https://github.com/daedaleanai/ublox/blob/main/ubx/messages.go + +// Message ubx-cfg-nav5 + +// CfgNav5 (Get/set) Navigation engine settings +// Class/Id 0x06 0x24 (36 bytes) +// See the Navigation Configuration Settings Description for a detailed description of how these settings affect receiver operation. +type CfgNav5 struct { + Mask CfgNav5Mask // Parameters bitmask. Only the masked parameters will be applied. + DynModel byte // Dynamic platform model: 0: portable 2: stationary 3: pedestrian 4: automotive 5: sea 6: airborne with <1g acceleration 7: airborne with <2g acceleration 8: airborne with <4g acceleration 9: wrist-worn watch (not supported in protocol versions less than 18) 10: bike (supported in protocol versions 19. 2) + FixMode byte // Position fixing mode: 1: 2D only 2: 3D only 3: auto 2D/3D + FixedAlt_me2 int32 // [1e-2 m] Fixed altitude (mean sea level) for 2D fix mode + FixedAltVar_m2e4 uint32 // [1e-4 m^2] Fixed altitude variance for 2D mode + MinElev_deg int8 // [deg] Minimum elevation for a GNSS satellite to be used in NAV + DrLimit_s byte // [s] Reserved + PDop uint16 // Position DOP mask to use + TDop uint16 // Time DOP mask to use + PAcc_m uint16 // [m] Position accuracy mask + TAcc_m uint16 // [m] Time accuracy mask + StaticHoldThresh_cm_s byte // [cm/s] Static hold threshold + DgnssTimeout_s byte // [s] DGNSS timeout + CnoThreshNumSVs byte // Number of satellites required to have C/N0 above cnoThresh for a fix to be attempted + CnoThresh_dbhz byte // [dBHz] C/N0 threshold for deciding whether to attempt a fix + Reserved1 [2]byte // Reserved + StaticHoldMaxDist_m uint16 // [m] Static hold distance threshold (before quitting static hold) + UtcStandard byte // UTC standard to be used: 0: Automatic; receiver selects based on GNSS configuration (see GNSS time bases) 3: UTC as operated by the U.S. Naval Observatory (USNO); derived from GPS time 5: UTC as combined from multiple European laboratories; derived from Galileo time 6: UTC as operated by the former Soviet Union (SU); derived from GLONASS time 7: UTC as operated by the National Time Service Center (NTSC), China; derived from BeiDou time (not supported in protocol versions less than 16). + Reserved2 [5]byte // Reserved +} + +func (CfgNav5) classID() uint16 { return 0x2406 } + +type CfgNav5Mask uint16 + +const ( + CfgNav5Dyn CfgNav5Mask = 0x1 // Apply dynamic model settings + CfgNav5MinEl CfgNav5Mask = 0x2 // Apply minimum elevation settings + CfgNav5PosFixMode CfgNav5Mask = 0x4 // Apply fix mode settings + CfgNav5DrLim CfgNav5Mask = 0x8 // Reserved + CfgNav5PosMask CfgNav5Mask = 0x10 // Apply position mask settings + CfgNav5TimeMask CfgNav5Mask = 0x20 // Apply time mask settings + CfgNav5StaticHoldMask CfgNav5Mask = 0x40 // Apply static hold settings + CfgNav5DgpsMask CfgNav5Mask = 0x80 // Apply DGPS settings + CfgNav5CnoThreshold CfgNav5Mask = 0x100 // Apply CNO threshold settings (cnoThresh, cnoThreshNumSVs) + CfgNav5Utc CfgNav5Mask = 0x400 // Apply UTC settings (not supported in protocol versions less than 16). +) + +// Write CfgNav5 message to buffer +func (cfg CfgNav5) Write(buf []byte) (int, error) { + copy(buf, []byte{0xb5, 0x62, byte(cfg.classID()), byte(cfg.classID() >> 8), 36, 0}) + + buf[6] = byte(cfg.Mask) + buf[7] = byte(cfg.Mask >> 8) + buf[8] = cfg.DynModel + buf[9] = cfg.FixMode + buf[10] = byte(cfg.FixedAlt_me2) + buf[11] = byte(cfg.FixedAlt_me2 >> 8) + buf[12] = byte(cfg.FixedAlt_me2 >> 16) + buf[13] = byte(cfg.FixedAlt_me2 >> 24) + buf[14] = byte(cfg.FixedAltVar_m2e4) + buf[15] = byte(cfg.FixedAltVar_m2e4 >> 8) + buf[16] = byte(cfg.FixedAltVar_m2e4 >> 16) + buf[17] = byte(cfg.FixedAltVar_m2e4 >> 24) + buf[18] = byte(cfg.MinElev_deg) + buf[19] = cfg.DrLimit_s + buf[20] = byte(cfg.PDop) + buf[21] = byte(cfg.PDop >> 8) + buf[22] = byte(cfg.TDop) + buf[23] = byte(cfg.TDop >> 8) + buf[24] = byte(cfg.PAcc_m) + buf[25] = byte(cfg.PAcc_m >> 8) + buf[26] = byte(cfg.TAcc_m) + buf[27] = byte(cfg.TAcc_m >> 8) + buf[28] = cfg.StaticHoldThresh_cm_s + buf[29] = cfg.DgnssTimeout_s + buf[30] = cfg.CnoThreshNumSVs + buf[31] = cfg.CnoThresh_dbhz + copy(buf[32:34], cfg.Reserved1[:]) + buf[34] = byte(cfg.StaticHoldMaxDist_m) + buf[35] = byte(cfg.StaticHoldMaxDist_m >> 8) + buf[36] = cfg.UtcStandard + copy(buf[37:42], cfg.Reserved2[:]) + + return 42, nil +} + +// Message ubx-cfg-msg + +// CfgMsg1 (Get/set) Set message rate +// Class/Id 0x06 0x01 (3 bytes) +// Set message rate configuration for the current port. See also section How to change between protocols. +type CfgMsg1 struct { + MsgClass byte // Message class + MsgID byte // Message identifier + Rate byte // Send rate on current port +} + +func (CfgMsg1) classID() uint16 { return 0x0106 } + +func (cfg CfgMsg1) Write(buf []byte) (int, error) { + copy(buf, []byte{0xb5, 0x62, byte(cfg.classID()), byte(cfg.classID() >> 8), 3, 0}) + + buf[6] = cfg.MsgClass + buf[7] = cfg.MsgID + buf[8] = cfg.Rate + + return 9, nil +} + +// Message ubx-cfg-gnss + +// CfgGnss (Get/set) GNSS system configuration +// Class/Id 0x06 0x3e (4 + N*8 bytes) +// Gets or sets the GNSS system channel sharing configuration. If the receiver is sent a valid new configuration, it will respond with a UBX-ACK- ACK message and immediately change to the new configuration. Otherwise the receiver will reject the request, by issuing a UBX-ACK-NAK and continuing operation with the previous configuration. Configuration requirements: It is necessary for at least one major GNSS to be enabled, after applying the new configuration to the current one. It is also required that at least 4 tracking channels are available to each enabled major GNSS, i.e. maxTrkCh must have a minimum value of 4 for each enabled major GNSS. The number of tracking channels in use must not exceed the number of tracking channels available in hardware, and the sum of all reserved tracking channels needs to be less than or equal to the number of tracking channels in use. Notes: To avoid cross-correlation issues, it is recommended that GPS and QZSS are always both enabled or both disabled. Polling this message returns the configuration of all supported GNSS, whether enabled or not; it may also include GNSS unsupported by the particular product, but in such cases the enable flag will always be unset. See section GNSS Configuration for a discussion of the use of this message. See section Satellite Numbering for a description of the GNSS IDs available. Configuration specific to the GNSS system can be done via other messages (e. g. UBX-CFG-SBAS). +type CfgGnss struct { + MsgVer byte // Message version (0x00 for this version) + NumTrkChHw byte // Number of tracking channels available in hardware (read only) + NumTrkChUse byte // (Read only in protocol versions greater than 23) Number of tracking channels to use. Must be > 0, <= numTrkChHw. If 0xFF, then number of tracking channels to use will be set to numTrkChHw. + NumConfigBlocks byte `len:"ConfigBlocks"` // Number of configuration blocks following + ConfigBlocks []*CfgGnssConfigBlocksType // len: NumConfigBlocks +} + +func (CfgGnss) classID() uint16 { return 0x3e06 } + +type CfgGnssConfigBlocksType struct { + GnssId byte // System identifier (see Satellite Numbering ) + ResTrkCh byte // (Read only in protocol versions greater than 23) Number of reserved (minimum) tracking channels for this system. + MaxTrkCh byte // (Read only in protocol versions greater than 23) Maximum number of tracking channels used for this system. Must be > 0, >= resTrkChn, <= numTrkChUse and <= maximum number of tracking channels supported for this system. + Reserved1 byte // Reserved + Flags CfgGnssFlags // Bitfield of flags. At least one signal must be configured in every enabled system. +} + +type CfgGnssFlags uint32 + +const ( + CfgGnssEnable CfgGnssFlags = 0x1 // Enable this system + CfgGnssSigCfgMask CfgGnssFlags = 0xff0000 // Signal configuration mask When gnssId is 0 (GPS) 0x01 = GPS L1C/A 0x10 = GPS L2C 0x20 = GPS L5 When gnssId is 1 (SBAS) 0x01 = SBAS L1C/A When gnssId is 2 (Galileo) 0x01 = Galileo E1 (not supported in protocol versions less than 18) 0x10 = Galileo E5a 0x20 = Galileo E5b When gnssId is 3 (BeiDou) 0x01 = BeiDou B1I 0x10 = BeiDou B2I 0x80 = BeiDou B2A When gnssId is 4 (IMES) 0x01 = IMES L1 When gnssId is 5 (QZSS) 0x01 = QZSS L1C/A 0x04 = QZSS L1S 0x10 = QZSS L2C 0x20 = QZSS L5 When gnssId is 6 (GLONASS) 0x01 = GLONASS L1 0x10 = GLONASS L2 +) + +// Write CfgGnss message to buffer +func (cfg CfgGnss) Write(buf []byte) (int, error) { + copy(buf, []byte{0xb5, 0x62, byte(cfg.classID()), byte(cfg.classID() >> 8), 4 + byte(len(cfg.ConfigBlocks))*8, 0}) + + buf[6] = cfg.MsgVer + buf[7] = cfg.NumTrkChHw + buf[8] = cfg.NumTrkChUse + buf[9] = byte(len(cfg.ConfigBlocks)) + + offset := 10 + for _, block := range cfg.ConfigBlocks { + buf[offset] = block.GnssId + buf[offset+1] = block.ResTrkCh + buf[offset+2] = block.MaxTrkCh + buf[offset+3] = block.Reserved1 + buf[offset+4] = byte(block.Flags) + buf[offset+5] = byte(block.Flags >> 8) + buf[offset+6] = byte(block.Flags >> 16) + buf[offset+7] = byte(block.Flags >> 24) + offset += 8 + } + + return offset, nil +} diff --git a/gps/ubx_test.go b/gps/ubx_test.go new file mode 100644 index 0000000..d2e74b6 --- /dev/null +++ b/gps/ubx_test.go @@ -0,0 +1,199 @@ +package gps + +import ( + "testing" +) + +func TestCfgNav5ClassID(t *testing.T) { + cfg := CfgNav5{} + if got := cfg.classID(); got != 0x2406 { + t.Errorf("expected 0x2406, got 0x%04x", got) + } +} + +func TestCfgNav5Write(t *testing.T) { + cfg := CfgNav5{ + Mask: CfgNav5Dyn | CfgNav5MinEl, + DynModel: 4, + FixMode: 3, + FixedAlt_me2: 10000, + FixedAltVar_m2e4: 10000, + MinElev_deg: 5, + DrLimit_s: 0, + PDop: 250, + TDop: 250, + PAcc_m: 100, + TAcc_m: 300, + StaticHoldThresh_cm_s: 50, + DgnssTimeout_s: 60, + CnoThreshNumSVs: 3, + CnoThresh_dbhz: 35, + Reserved1: [2]byte{0, 0}, + StaticHoldMaxDist_m: 200, + UtcStandard: 0, + Reserved2: [5]byte{0, 0, 0, 0, 0}, + } + + buf := make([]byte, 64) + n, err := cfg.Write(buf) + + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if n != 42 { + t.Errorf("expected 42 bytes written, got %d", n) + } + + // Check sync chars + if buf[0] != 0xb5 || buf[1] != 0x62 { + t.Errorf("expected sync chars 0xb5 0x62, got 0x%02x 0x%02x", buf[0], buf[1]) + } + + // Check class/id (little-endian) + if buf[2] != 0x06 || buf[3] != 0x24 { + t.Errorf("expected class/id 0x06 0x24, got 0x%02x 0x%02x", buf[2], buf[3]) + } + + // Check length + if buf[4] != 36 || buf[5] != 0 { + t.Errorf("expected length 36, got %d", uint16(buf[4])|uint16(buf[5])<<8) + } + + // Check Mask (little-endian) + mask := uint16(buf[6]) | uint16(buf[7])<<8 + if mask != uint16(CfgNav5Dyn|CfgNav5MinEl) { + t.Errorf("expected mask 0x03, got 0x%04x", mask) + } + + // Check DynModel + if buf[8] != 4 { + t.Errorf("expected DynModel 4, got %d", buf[8]) + } + + // Check FixMode + if buf[9] != 3 { + t.Errorf("expected FixMode 3, got %d", buf[9]) + } + + // Check FixedAlt_me2 (little-endian int32) + fixedAlt := int32(buf[10]) | int32(buf[11])<<8 | int32(buf[12])<<16 | int32(buf[13])<<24 + if fixedAlt != 10000 { + t.Errorf("expected FixedAlt_me2 10000, got %d", fixedAlt) + } +} + +func TestCfgGnssClassID(t *testing.T) { + cfg := CfgGnss{} + if got := cfg.classID(); got != 0x3e06 { + t.Errorf("expected 0x3e06, got 0x%04x", got) + } +} + +func TestCfgGnssWrite(t *testing.T) { + testCases := []struct { + name string + cfg CfgGnss + expectedLen int + expectedBlocks byte + }{ + { + name: "no config blocks", + cfg: CfgGnss{ + MsgVer: 0, + NumTrkChHw: 32, + NumTrkChUse: 32, + ConfigBlocks: nil, + }, + expectedLen: 10, + expectedBlocks: 0, + }, + { + name: "one config block", + cfg: CfgGnss{ + MsgVer: 0, + NumTrkChHw: 32, + NumTrkChUse: 32, + ConfigBlocks: []*CfgGnssConfigBlocksType{ + {GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Flags: CfgGnssEnable | 0x010000}, + }, + }, + expectedLen: 18, + expectedBlocks: 1, + }, + { + name: "two config blocks", + cfg: CfgGnss{ + MsgVer: 0, + NumTrkChHw: 32, + NumTrkChUse: 32, + ConfigBlocks: []*CfgGnssConfigBlocksType{ + {GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Flags: CfgGnssEnable | 0x010000}, + {GnssId: 6, ResTrkCh: 8, MaxTrkCh: 14, Flags: CfgGnssEnable | 0x010000}, + }, + }, + expectedLen: 26, + expectedBlocks: 2, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + buf := make([]byte, 64) + n, err := tc.cfg.Write(buf) + + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if n != tc.expectedLen { + t.Errorf("expected %d bytes written, got %d", tc.expectedLen, n) + } + + // Check sync chars + if buf[0] != 0xb5 || buf[1] != 0x62 { + t.Errorf("expected sync chars 0xb5 0x62, got 0x%02x 0x%02x", buf[0], buf[1]) + } + + // Check class/id (little-endian) + if buf[2] != 0x06 || buf[3] != 0x3e { + t.Errorf("expected class/id 0x06 0x3e, got 0x%02x 0x%02x", buf[2], buf[3]) + } + + // Check number of config blocks + if buf[9] != tc.expectedBlocks { + t.Errorf("expected %d config blocks, got %d", tc.expectedBlocks, buf[9]) + } + }) + } +} + +func TestCfgGnssWriteBlockContent(t *testing.T) { + cfg := CfgGnss{ + MsgVer: 0, + NumTrkChHw: 32, + NumTrkChUse: 32, + ConfigBlocks: []*CfgGnssConfigBlocksType{ + {GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Reserved1: 0, Flags: CfgGnssEnable | 0x010000}, + }, + } + + buf := make([]byte, 64) + _, _ = cfg.Write(buf) + + // Check first block at offset 10 + if buf[10] != 0 { + t.Errorf("expected GnssId 0, got %d", buf[10]) + } + if buf[11] != 8 { + t.Errorf("expected ResTrkCh 8, got %d", buf[11]) + } + if buf[12] != 16 { + t.Errorf("expected MaxTrkCh 16, got %d", buf[12]) + } + + // Check flags (little-endian uint32) + flags := uint32(buf[14]) | uint32(buf[15])<<8 | uint32(buf[16])<<16 | uint32(buf[17])<<24 + expectedFlags := uint32(CfgGnssEnable | 0x010000) + if flags != expectedFlags { + t.Errorf("expected flags 0x%08x, got 0x%08x", expectedFlags, flags) + } +} From 151380842578e50d89c92e5c488f058eacff44e2 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 30 Dec 2025 17:56:04 +0100 Subject: [PATCH 113/137] gps: revamp validSentence() to avoid heap allocation for errors. This error can occur too frequently to allow for such allocations Signed-off-by: deadprogram --- gps/gps.go | 27 ++------------------------- gps/gpsparser.go | 2 +- gps/gpsparser_test.go | 6 +++--- 3 files changed, 6 insertions(+), 29 deletions(-) diff --git a/gps/gps.go b/gps/gps.go index 0d196c4..4133c96 100644 --- a/gps/gps.go +++ b/gps/gps.go @@ -20,30 +20,9 @@ var ( errInvalidGLLSentence = errors.New("invalid GLL NMEA sentence") errGPSCommandRejected = errors.New("GPS command rejected (NAK)") errNoACKToGPSCommand = errors.New("no ACK to GPS command") + errInvalidNMEASentanceFormat = errors.New("invalid NMEA sentence format") ) -type GPSError struct { - Err error - Info string - Sentence string -} - -func newGPSError(err error, sentence string, info string) GPSError { - return GPSError{ - Info: info, - Err: err, - Sentence: sentence, - } -} - -func (ge GPSError) Error() string { - return ge.Err.Error() + " " + ge.Info + " " + ge.Sentence -} - -func (ge GPSError) Unwrap() error { - return ge.Err -} - const ( minimumNMEALength = 7 startingDelimiter = '$' @@ -182,9 +161,7 @@ func validSentence(sentence string) error { } checksum := strings.ToUpper(hex.EncodeToString([]byte{cs})) if checksum != sentence[len(sentence)-2:len(sentence)] { - return newGPSError(errInvalidNMEAChecksum, sentence, - "expected "+sentence[len(sentence)-2:len(sentence)]+ - " got "+checksum) + return errInvalidNMEASentanceFormat } return nil diff --git a/gps/gpsparser.go b/gps/gpsparser.go index 6522a5f..8434587 100644 --- a/gps/gpsparser.go +++ b/gps/gpsparser.go @@ -104,7 +104,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) { return fix, nil } - return fix, newGPSError(errUnknownNMEASentence, sentence, typ) + return fix, errInvalidNMEASentanceFormat } // findTime returns the time from an NMEA sentence: diff --git a/gps/gpsparser_test.go b/gps/gpsparser_test.go index 9544312..8e035aa 100644 --- a/gps/gpsparser_test.go +++ b/gps/gpsparser_test.go @@ -8,13 +8,13 @@ import ( ) func TestParseUnknownSentence(t *testing.T) { - c := qt.New(t) - p := NewParser() val := "$GPGSV,3,1,09,07,14,317,22,08,31,284,25,10,32,133,39,16,85,232,29*7F" _, err := p.Parse(val) - c.Assert(err.Error(), qt.Contains, "unsupported NMEA sentence type") + if err == nil { + t.Error("should have unknown sentence err") + } } func TestParseGGA(t *testing.T) { From b390e3225a56a09f19ba7147f53c72e8fb483b07 Mon Sep 17 00:00:00 2001 From: Pat Whittingslow Date: Thu, 8 Jan 2026 09:21:44 -0300 Subject: [PATCH 114/137] Suggestions by pato for GPS UBX support (#831) * apply suggestions by pato * whoopsie on inverted condition --- gps/gps.go | 4 +- gps/ublox.go | 102 ++++++++++++++++-------------------- gps/ublox_test.go | 131 ++++++++++++++++++---------------------------- gps/ubx.go | 54 +++++++++++++------ gps/ubx_test.go | 30 ++++------- 5 files changed, 145 insertions(+), 176 deletions(-) diff --git a/gps/gps.go b/gps/gps.go index 4133c96..bc81797 100644 --- a/gps/gps.go +++ b/gps/gps.go @@ -31,19 +31,18 @@ const ( // Device wraps a connection to a GPS device. type Device struct { - buffer []byte bufIdx int sentence strings.Builder uart drivers.UART bus drivers.I2C address uint16 + buffer [bufferSize]byte } // NewUART creates a new UART GPS connection. The UART must already be configured. func NewUART(uart drivers.UART) Device { return Device{ uart: uart, - buffer: make([]byte, bufferSize), bufIdx: bufferSize, sentence: strings.Builder{}, } @@ -60,7 +59,6 @@ func NewI2CWithAddress(bus drivers.I2C, i2cAddress uint16) Device { return Device{ bus: bus, address: i2cAddress, - buffer: make([]byte, bufferSize), bufIdx: bufferSize, sentence: strings.Builder{}, } diff --git a/gps/ublox.go b/gps/ublox.go index c74ceb1..ce6f020 100644 --- a/gps/ublox.go +++ b/gps/ublox.go @@ -6,7 +6,7 @@ import ( // FlightModeCmd is a UBX-CFG-NAV5 command to set the GPS into // flight mode (airborne <1g) -var FlightModeCmd = CfgNav5{ +var flightModeCmd = CfgNav5{ Mask: CfgNav5Dyn | CfgNav5MinEl | CfgNav5PosFixMode, DynModel: DynModeAirborne1g, // Airborne with <1g acceleration FixMode: FixModeAuto, // Auto 2D/3D @@ -28,59 +28,56 @@ var FlightModeCmd = CfgNav5{ } // SetFlightMode sends UBX-CFG-NAV5 command to set GPS into flight mode -func SetFlightMode(d Device) (err error) { - if _, err = FlightModeCmd.Write(d.buffer[:]); err != nil { - return err - } - err = SendCommand(d, d.buffer[:]) - return err +func (d *Device) SetFlightMode() (err error) { + flightModeCmd.Put42Bytes(d.buffer[:]) + return d.SendCommand(d.buffer[:42]) } var ( // GGA (time, lat/lng, altitude) - MessageRateGGACmd = CfgMsg1{ + messageRateGGACmd = CfgMsg1{ MsgClass: 0xF0, MsgID: 0x00, Rate: 1, // Every position fix } // GLL (time, lat/lng) - MessageRateGLLCmd = CfgMsg1{ + messageRateGLLCmd = CfgMsg1{ MsgClass: 0xF0, MsgID: 0x01, Rate: 0, // Disabled } // GSA (satellite id list) - MessageRateGSACmd = CfgMsg1{ + messageRateGSACmd = CfgMsg1{ MsgClass: 0xF0, MsgID: 0x02, Rate: 1, // Every position fix } // GSV (satellite locations) - MessageRateGSVCmd = CfgMsg1{ + messageRateGSVCmd = CfgMsg1{ MsgClass: 0xF0, MsgID: 0x03, Rate: 1, // Every position fix } // RMC (time, lat/lng, speed, course) - MessageRateRMCCmd = CfgMsg1{ + messageRateRMCCmd = CfgMsg1{ MsgClass: 0xF0, MsgID: 0x04, Rate: 1, // Every position fix } // VTG (speed, course) - MessageRateVTGCmd = CfgMsg1{ + messageRateVTGCmd = CfgMsg1{ MsgClass: 0xF0, MsgID: 0x05, Rate: 0, // Disabled } // ZDA (time, timezone) - MessageRateZDACmd = CfgMsg1{ + messageRateZDACmd = CfgMsg1{ MsgClass: 0xF0, MsgID: 0x08, Rate: 0, // Disabled } // TXT (text transmission) - MessageRateTXTCmd = CfgMsg1{ + messageRateTXTCmd = CfgMsg1{ MsgClass: 0xF0, MsgID: 0x41, Rate: 0, // Disabled @@ -88,63 +85,53 @@ var ( ) // SetMessageRatesMinimal configures the GPS to output a minimal set of NMEA sentences -func SetMessageRatesMinimal(d Device) (err error) { +func SetMessageRatesMinimal(d *Device) (err error) { commands := []CfgMsg1{ - MessageRateGSACmd, - MessageRateGGACmd, - MessageRateGLLCmd, - MessageRateGSVCmd, - MessageRateRMCCmd, - MessageRateVTGCmd, - MessageRateZDACmd, - MessageRateTXTCmd, + messageRateGSACmd, + messageRateGGACmd, + messageRateGLLCmd, + messageRateGSVCmd, + messageRateRMCCmd, + messageRateVTGCmd, + messageRateZDACmd, + messageRateTXTCmd, } - - for _, cmd := range commands { - if _, err = cmd.Write(d.buffer[:]); err != nil { - return err - } - if err = SendCommand(d, d.buffer[:]); err != nil { - return err - } - } - - return nil + return setCfg1s(d, commands) } // SetMessageRatesAllEnabled configures the GPS to output all NMEA sentences -func SetMessageRatesAllEnabled(d Device) (err error) { +func SetMessageRatesAllEnabled(d *Device) (err error) { commands := []CfgMsg1{ - MessageRateGSACmd, - MessageRateGGACmd, - MessageRateGLLCmd, - MessageRateGSVCmd, - MessageRateRMCCmd, - MessageRateVTGCmd, - MessageRateZDACmd, - MessageRateTXTCmd, + messageRateGSACmd, + messageRateGGACmd, + messageRateGLLCmd, + messageRateGSVCmd, + messageRateRMCCmd, + messageRateVTGCmd, + messageRateZDACmd, + messageRateTXTCmd, } + return setCfg1s(d, commands) +} +func setCfg1s(d *Device, commands []CfgMsg1) (err error) { + var buf [9]byte for _, cmd := range commands { - cmd.Rate = 1 // Enable all messages at 1 Hz - if _, err = cmd.Write(d.buffer[:]); err != nil { - return err - } - if err = SendCommand(d, d.buffer[:]); err != nil { + cmd.Put9Bytes(buf[:9]) + if err = d.SendCommand(buf[:9]); err != nil { return err } } - return nil } -// GNSSDisableCmd is a UBX-CFG-GNSS command to disable all GNSS but GPS +// gnssDisableCmd is a UBX-CFG-GNSS command to disable all GNSS but GPS // Needed for MAX8's, not needed for MAX7 -var GNSSDisableCmd = CfgGnss{ +var gnssDisableCmd = CfgGnss{ MsgVer: 0x00, NumTrkChHw: 0x20, // 32 channels NumTrkChUse: 0x20, - ConfigBlocks: []*CfgGnssConfigBlocksType{ + ConfigBlocks: []CfgGnssConfigBlocksType{ {GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Flags: CfgGnssEnable | 0x010000}, // GPS enabled {GnssId: 1, ResTrkCh: 1, MaxTrkCh: 3, Flags: 0x010000}, // SBAS disabled {GnssId: 3, ResTrkCh: 8, MaxTrkCh: 16, Flags: 0x010000}, // BeiDou disabled @@ -154,15 +141,16 @@ var GNSSDisableCmd = CfgGnss{ } // SetGNSSDisable sends UBX-CFG-GNSS command to disable all GNSS but GPS -func SetGNSSDisable(d Device) (err error) { - if _, err = GNSSDisableCmd.Write(d.buffer[:]); err != nil { +func (d *Device) SetGNSSDisable() (err error) { + err = gnssDisableCmd.Put(d.buffer[:]) + if err != nil { return err } - return SendCommand(d, d.buffer[:]) + return d.SendCommand(d.buffer[:]) } // SendCommand sends a UBX command and waits for ACK/NAK response -func SendCommand(d Device, command []byte) error { +func (d *Device) SendCommand(command []byte) error { // Calculate and append checksum checksummed := appendChecksum(command) d.WriteBytes(checksummed) diff --git a/gps/ublox_test.go b/gps/ublox_test.go index 73648d6..1101e89 100644 --- a/gps/ublox_test.go +++ b/gps/ublox_test.go @@ -67,41 +67,41 @@ func TestAppendChecksumPreservesOriginal(t *testing.T) { func TestFlightModeCmdConfig(t *testing.T) { // Verify FlightModeCmd has expected values - if FlightModeCmd.DynModel != 6 { - t.Errorf("expected DynModel 6 (airborne <1g), got %d", FlightModeCmd.DynModel) + if flightModeCmd.DynModel != 6 { + t.Errorf("expected DynModel 6 (airborne <1g), got %d", flightModeCmd.DynModel) } - if FlightModeCmd.FixMode != 3 { - t.Errorf("expected FixMode 3 (auto 2D/3D), got %d", FlightModeCmd.FixMode) + if flightModeCmd.FixMode != 3 { + t.Errorf("expected FixMode 3 (auto 2D/3D), got %d", flightModeCmd.FixMode) } expectedMask := CfgNav5Dyn | CfgNav5MinEl | CfgNav5PosFixMode - if FlightModeCmd.Mask != expectedMask { - t.Errorf("expected Mask 0x%04X, got 0x%04X", expectedMask, FlightModeCmd.Mask) + if flightModeCmd.Mask != expectedMask { + t.Errorf("expected Mask 0x%04X, got 0x%04X", expectedMask, flightModeCmd.Mask) } - if FlightModeCmd.MinElev_deg != 5 { - t.Errorf("expected MinElev_deg 5, got %d", FlightModeCmd.MinElev_deg) + if flightModeCmd.MinElev_deg != 5 { + t.Errorf("expected MinElev_deg 5, got %d", flightModeCmd.MinElev_deg) } } func TestGNSSDisableCmdConfig(t *testing.T) { // Verify GNSSDisableCmd has expected structure - if GNSSDisableCmd.MsgVer != 0 { - t.Errorf("expected MsgVer 0, got %d", GNSSDisableCmd.MsgVer) + if gnssDisableCmd.MsgVer != 0 { + t.Errorf("expected MsgVer 0, got %d", gnssDisableCmd.MsgVer) } - if GNSSDisableCmd.NumTrkChHw != 0x20 { - t.Errorf("expected NumTrkChHw 0x20, got 0x%02X", GNSSDisableCmd.NumTrkChHw) + if gnssDisableCmd.NumTrkChHw != 0x20 { + t.Errorf("expected NumTrkChHw 0x20, got 0x%02X", gnssDisableCmd.NumTrkChHw) } - if len(GNSSDisableCmd.ConfigBlocks) != 5 { - t.Errorf("expected 5 config blocks, got %d", len(GNSSDisableCmd.ConfigBlocks)) + if len(gnssDisableCmd.ConfigBlocks) != 5 { + t.Errorf("expected 5 config blocks, got %d", len(gnssDisableCmd.ConfigBlocks)) return } // Verify GPS is enabled - gpsBlock := GNSSDisableCmd.ConfigBlocks[0] + gpsBlock := gnssDisableCmd.ConfigBlocks[0] if gpsBlock.GnssId != 0 { t.Errorf("expected first block GnssId 0 (GPS), got %d", gpsBlock.GnssId) } @@ -110,8 +110,8 @@ func TestGNSSDisableCmdConfig(t *testing.T) { } // Verify other GNSS are disabled - for i := 1; i < len(GNSSDisableCmd.ConfigBlocks); i++ { - block := GNSSDisableCmd.ConfigBlocks[i] + for i := 1; i < len(gnssDisableCmd.ConfigBlocks); i++ { + block := gnssDisableCmd.ConfigBlocks[i] if block.Flags&CfgGnssEnable != 0 { t.Errorf("expected block %d (GnssId %d) to be disabled", i, block.GnssId) } @@ -120,15 +120,7 @@ func TestGNSSDisableCmdConfig(t *testing.T) { func TestFlightModeCmdWrite(t *testing.T) { buf := make([]byte, 64) - n, err := FlightModeCmd.Write(buf) - - if err != nil { - t.Errorf("unexpected error: %v", err) - } - - if n != 42 { - t.Errorf("expected 42 bytes, got %d", n) - } + flightModeCmd.Put42Bytes(buf) // Verify sync chars if buf[0] != 0xB5 || buf[1] != 0x62 { @@ -148,16 +140,16 @@ func TestFlightModeCmdWrite(t *testing.T) { func TestGNSSDisableCmdWrite(t *testing.T) { buf := make([]byte, 64) - n, err := GNSSDisableCmd.Write(buf) - + err := gnssDisableCmd.Put(buf) if err != nil { - t.Errorf("unexpected error: %v", err) + t.Errorf("unexpected error, likely buffer too short for data: %v", err) } // 6 header + 4 payload header + 5*8 blocks = 50 bytes - expectedLen := 6 + 4 + 5*8 - if n != expectedLen { - t.Errorf("expected %d bytes, got %d", expectedLen, n) + const expectedLen = 6 + 4 + 5*8 + sz := gnssDisableCmd.Size() + if sz != expectedLen { + t.Errorf("expected %d bytes, got %d", expectedLen, sz) } // Verify sync chars @@ -204,14 +196,14 @@ func TestMessageRateCmdConfigs(t *testing.T) { msgID byte rate byte }{ - {"GGA", MessageRateGGACmd, 0xF0, 0x00, 1}, - {"GLL", MessageRateGLLCmd, 0xF0, 0x01, 0}, - {"GSA", MessageRateGSACmd, 0xF0, 0x02, 1}, - {"GSV", MessageRateGSVCmd, 0xF0, 0x03, 1}, - {"RMC", MessageRateRMCCmd, 0xF0, 0x04, 1}, - {"VTG", MessageRateVTGCmd, 0xF0, 0x05, 0}, - {"ZDA", MessageRateZDACmd, 0xF0, 0x08, 0}, - {"TXT", MessageRateTXTCmd, 0xF0, 0x41, 0}, + {"GGA", messageRateGGACmd, 0xF0, 0x00, 1}, + {"GLL", messageRateGLLCmd, 0xF0, 0x01, 0}, + {"GSA", messageRateGSACmd, 0xF0, 0x02, 1}, + {"GSV", messageRateGSVCmd, 0xF0, 0x03, 1}, + {"RMC", messageRateRMCCmd, 0xF0, 0x04, 1}, + {"VTG", messageRateVTGCmd, 0xF0, 0x05, 0}, + {"ZDA", messageRateZDACmd, 0xF0, 0x08, 0}, + {"TXT", messageRateTXTCmd, 0xF0, 0x41, 0}, } for _, tc := range testCases { @@ -237,16 +229,7 @@ func TestCfgMsg1Write(t *testing.T) { } buf := make([]byte, 16) - n, err := cmd.Write(buf) - - if err != nil { - t.Errorf("unexpected error: %v", err) - } - - if n != 9 { - t.Errorf("expected 9 bytes, got %d", n) - } - + cmd.Put9Bytes(buf) // Verify sync chars if buf[0] != 0xB5 || buf[1] != 0x62 { t.Errorf("expected sync 0xB5 0x62, got 0x%02X 0x%02X", buf[0], buf[1]) @@ -296,14 +279,14 @@ func TestMinimalMessageRatesConfig(t *testing.T) { } commands := []CfgMsg1{ - MessageRateGGACmd, - MessageRateGLLCmd, - MessageRateGSACmd, - MessageRateGSVCmd, - MessageRateRMCCmd, - MessageRateVTGCmd, - MessageRateZDACmd, - MessageRateTXTCmd, + messageRateGGACmd, + messageRateGLLCmd, + messageRateGSACmd, + messageRateGSVCmd, + messageRateRMCCmd, + messageRateVTGCmd, + messageRateZDACmd, + messageRateTXTCmd, } for _, cmd := range commands { @@ -321,28 +304,19 @@ func TestMinimalMessageRatesConfig(t *testing.T) { func TestAllMessageRatesWriteCorrectBytes(t *testing.T) { // Test that each message rate command writes the correct bytes commands := []CfgMsg1{ - MessageRateGGACmd, - MessageRateGLLCmd, - MessageRateGSACmd, - MessageRateGSVCmd, - MessageRateRMCCmd, - MessageRateVTGCmd, - MessageRateZDACmd, - MessageRateTXTCmd, + messageRateGGACmd, + messageRateGLLCmd, + messageRateGSACmd, + messageRateGSVCmd, + messageRateRMCCmd, + messageRateVTGCmd, + messageRateZDACmd, + messageRateTXTCmd, } for _, cmd := range commands { buf := make([]byte, 16) - n, err := cmd.Write(buf) - - if err != nil { - t.Errorf("MsgID 0x%02X: unexpected error: %v", cmd.MsgID, err) - continue - } - - if n != 9 { - t.Errorf("MsgID 0x%02X: expected 9 bytes, got %d", cmd.MsgID, n) - } + cmd.Put9Bytes(buf) // Verify MsgClass in payload if buf[6] != 0xF0 { @@ -363,7 +337,7 @@ func TestAllMessageRatesWriteCorrectBytes(t *testing.T) { func TestSetMessageRatesAllEnabledModifiesRate(t *testing.T) { // Verify that when we copy a command and set Rate=1, it works correctly - cmd := MessageRateGLLCmd // This one is disabled by default + cmd := messageRateGLLCmd // This one is disabled by default if cmd.Rate != 0 { t.Errorf("expected GLL default rate 0, got %d", cmd.Rate) } @@ -372,8 +346,7 @@ func TestSetMessageRatesAllEnabledModifiesRate(t *testing.T) { cmd.Rate = 1 buf := make([]byte, 16) - _, _ = cmd.Write(buf) - + cmd.Put9Bytes(buf) if buf[8] != 1 { t.Errorf("expected Rate 1 in buffer, got %d", buf[8]) } diff --git a/gps/ubx.go b/gps/ubx.go index b84625f..e044d62 100644 --- a/gps/ubx.go +++ b/gps/ubx.go @@ -1,5 +1,7 @@ package gps +import "io" + // UBX message classes const ( ubxClassACK = 0x05 @@ -69,6 +71,8 @@ func (CfgNav5) classID() uint16 { return 0x2406 } type CfgNav5Mask uint16 +var _ io.WriterTo = CfgNav5{} // compile time guarantee of interface implementation. + const ( CfgNav5Dyn CfgNav5Mask = 0x1 // Apply dynamic model settings CfgNav5MinEl CfgNav5Mask = 0x2 // Apply minimum elevation settings @@ -82,8 +86,23 @@ const ( CfgNav5Utc CfgNav5Mask = 0x400 // Apply UTC settings (not supported in protocol versions less than 16). ) +func (cfg CfgNav5) Append(dst []byte) []byte { + var buf [42]byte + cfg.Put42Bytes(buf[:]) + dst = append(dst, buf[:]...) + return dst +} + +func (cfg CfgNav5) WriteTo(w io.Writer) (int64, error) { + var buf [42]byte + cfg.Put42Bytes(buf[:]) + n, err := w.Write(buf[:]) + return int64(n), err +} + // Write CfgNav5 message to buffer -func (cfg CfgNav5) Write(buf []byte) (int, error) { +func (cfg CfgNav5) Put42Bytes(buf []byte) { + _ = buf[41] copy(buf, []byte{0xb5, 0x62, byte(cfg.classID()), byte(cfg.classID() >> 8), 36, 0}) buf[6] = byte(cfg.Mask) @@ -117,8 +136,6 @@ func (cfg CfgNav5) Write(buf []byte) (int, error) { buf[35] = byte(cfg.StaticHoldMaxDist_m >> 8) buf[36] = cfg.UtcStandard copy(buf[37:42], cfg.Reserved2[:]) - - return 42, nil } // Message ubx-cfg-msg @@ -134,14 +151,11 @@ type CfgMsg1 struct { func (CfgMsg1) classID() uint16 { return 0x0106 } -func (cfg CfgMsg1) Write(buf []byte) (int, error) { +func (cfg CfgMsg1) Put9Bytes(buf []byte) { copy(buf, []byte{0xb5, 0x62, byte(cfg.classID()), byte(cfg.classID() >> 8), 3, 0}) - buf[6] = cfg.MsgClass buf[7] = cfg.MsgID buf[8] = cfg.Rate - - return 9, nil } // Message ubx-cfg-gnss @@ -150,11 +164,11 @@ func (cfg CfgMsg1) Write(buf []byte) (int, error) { // Class/Id 0x06 0x3e (4 + N*8 bytes) // Gets or sets the GNSS system channel sharing configuration. If the receiver is sent a valid new configuration, it will respond with a UBX-ACK- ACK message and immediately change to the new configuration. Otherwise the receiver will reject the request, by issuing a UBX-ACK-NAK and continuing operation with the previous configuration. Configuration requirements: It is necessary for at least one major GNSS to be enabled, after applying the new configuration to the current one. It is also required that at least 4 tracking channels are available to each enabled major GNSS, i.e. maxTrkCh must have a minimum value of 4 for each enabled major GNSS. The number of tracking channels in use must not exceed the number of tracking channels available in hardware, and the sum of all reserved tracking channels needs to be less than or equal to the number of tracking channels in use. Notes: To avoid cross-correlation issues, it is recommended that GPS and QZSS are always both enabled or both disabled. Polling this message returns the configuration of all supported GNSS, whether enabled or not; it may also include GNSS unsupported by the particular product, but in such cases the enable flag will always be unset. See section GNSS Configuration for a discussion of the use of this message. See section Satellite Numbering for a description of the GNSS IDs available. Configuration specific to the GNSS system can be done via other messages (e. g. UBX-CFG-SBAS). type CfgGnss struct { - MsgVer byte // Message version (0x00 for this version) - NumTrkChHw byte // Number of tracking channels available in hardware (read only) - NumTrkChUse byte // (Read only in protocol versions greater than 23) Number of tracking channels to use. Must be > 0, <= numTrkChHw. If 0xFF, then number of tracking channels to use will be set to numTrkChHw. - NumConfigBlocks byte `len:"ConfigBlocks"` // Number of configuration blocks following - ConfigBlocks []*CfgGnssConfigBlocksType // len: NumConfigBlocks + MsgVer byte // Message version (0x00 for this version) + NumTrkChHw byte // Number of tracking channels available in hardware (read only) + NumTrkChUse byte // (Read only in protocol versions greater than 23) Number of tracking channels to use. Must be > 0, <= numTrkChHw. If 0xFF, then number of tracking channels to use will be set to numTrkChHw. + NumConfigBlocks byte `len:"ConfigBlocks"` // Number of configuration blocks following + ConfigBlocks []CfgGnssConfigBlocksType // len: NumConfigBlocks } func (CfgGnss) classID() uint16 { return 0x3e06 } @@ -175,14 +189,16 @@ const ( ) // Write CfgGnss message to buffer -func (cfg CfgGnss) Write(buf []byte) (int, error) { +func (cfg CfgGnss) Put(buf []byte) error { + sz := cfg.Size() + if sz > len(buf) { + return io.ErrShortBuffer + } copy(buf, []byte{0xb5, 0x62, byte(cfg.classID()), byte(cfg.classID() >> 8), 4 + byte(len(cfg.ConfigBlocks))*8, 0}) - buf[6] = cfg.MsgVer buf[7] = cfg.NumTrkChHw buf[8] = cfg.NumTrkChUse buf[9] = byte(len(cfg.ConfigBlocks)) - offset := 10 for _, block := range cfg.ConfigBlocks { buf[offset] = block.GnssId @@ -195,6 +211,10 @@ func (cfg CfgGnss) Write(buf []byte) (int, error) { buf[offset+7] = byte(block.Flags >> 24) offset += 8 } - - return offset, nil + return nil +} + +// Size returns length of CfgGnss in bytes when sent over the wire. +func (cfg CfgGnss) Size() int { + return 10 + 8*len(cfg.ConfigBlocks) } diff --git a/gps/ubx_test.go b/gps/ubx_test.go index d2e74b6..ac8c61f 100644 --- a/gps/ubx_test.go +++ b/gps/ubx_test.go @@ -35,14 +35,7 @@ func TestCfgNav5Write(t *testing.T) { } buf := make([]byte, 64) - n, err := cfg.Write(buf) - - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if n != 42 { - t.Errorf("expected 42 bytes written, got %d", n) - } + cfg.Put42Bytes(buf) // Check sync chars if buf[0] != 0xb5 || buf[1] != 0x62 { @@ -113,7 +106,7 @@ func TestCfgGnssWrite(t *testing.T) { MsgVer: 0, NumTrkChHw: 32, NumTrkChUse: 32, - ConfigBlocks: []*CfgGnssConfigBlocksType{ + ConfigBlocks: []CfgGnssConfigBlocksType{ {GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Flags: CfgGnssEnable | 0x010000}, }, }, @@ -126,7 +119,7 @@ func TestCfgGnssWrite(t *testing.T) { MsgVer: 0, NumTrkChHw: 32, NumTrkChUse: 32, - ConfigBlocks: []*CfgGnssConfigBlocksType{ + ConfigBlocks: []CfgGnssConfigBlocksType{ {GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Flags: CfgGnssEnable | 0x010000}, {GnssId: 6, ResTrkCh: 8, MaxTrkCh: 14, Flags: CfgGnssEnable | 0x010000}, }, @@ -139,15 +132,10 @@ func TestCfgGnssWrite(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { buf := make([]byte, 64) - n, err := tc.cfg.Write(buf) - + err := tc.cfg.Put(buf) if err != nil { - t.Errorf("unexpected error: %v", err) + t.Errorf("unexpected error, data too long?: %v", err) } - if n != tc.expectedLen { - t.Errorf("expected %d bytes written, got %d", tc.expectedLen, n) - } - // Check sync chars if buf[0] != 0xb5 || buf[1] != 0x62 { t.Errorf("expected sync chars 0xb5 0x62, got 0x%02x 0x%02x", buf[0], buf[1]) @@ -171,14 +159,16 @@ func TestCfgGnssWriteBlockContent(t *testing.T) { MsgVer: 0, NumTrkChHw: 32, NumTrkChUse: 32, - ConfigBlocks: []*CfgGnssConfigBlocksType{ + ConfigBlocks: []CfgGnssConfigBlocksType{ {GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Reserved1: 0, Flags: CfgGnssEnable | 0x010000}, }, } buf := make([]byte, 64) - _, _ = cfg.Write(buf) - + err := cfg.Put(buf) + if err != nil { + t.Fatal(err) + } // Check first block at offset 10 if buf[10] != 0 { t.Errorf("expected GnssId 0, got %d", buf[10]) From 892265b7332bf9eee098b741c9f3413ac0016d53 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Thu, 8 Jan 2026 13:49:30 +0100 Subject: [PATCH 115/137] gps: export some errors for checking/supression from client Signed-off-by: deadprogram --- examples/gps/uart/main.go | 21 +++++++++++++++------ gps/gps.go | 15 +++++++-------- gps/gpsparser.go | 6 +++--- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/examples/gps/uart/main.go b/examples/gps/uart/main.go index 7e6a2c4..c5309bf 100644 --- a/examples/gps/uart/main.go +++ b/examples/gps/uart/main.go @@ -8,7 +8,6 @@ import ( ) func main() { - println("GPS UART Example") machine.UART1.Configure(machine.UARTConfig{BaudRate: 9600}) ublox := gps.NewUART(machine.UART1) parser := gps.NewParser() @@ -16,14 +15,24 @@ func main() { for { s, err := ublox.NextSentence() if err != nil { - println(err) - continue + switch err { + case gps.ErrUnknownNMEASentence, gps.ErrInvalidNMEASentence, gps.ErrInvalidNMEASentenceLength: + continue + default: + println("sentence error:", err) + continue + } } fix, err = parser.Parse(s) if err != nil { - println(err) - continue + switch err { + case gps.ErrUnknownNMEASentence, gps.ErrInvalidNMEASentence, gps.ErrInvalidNMEASentenceLength: + continue + default: + println("parse error:", err) + continue + } } if fix.Valid { print(fix.Time.Format("15:04:05")) @@ -43,7 +52,7 @@ func main() { } println() } else { - println("No fix") + println("Waiting for fix...") } time.Sleep(200 * time.Millisecond) } diff --git a/gps/gps.go b/gps/gps.go index bc81797..b064bbe 100644 --- a/gps/gps.go +++ b/gps/gps.go @@ -11,16 +11,15 @@ import ( ) var ( - errInvalidNMEASentenceLength = errors.New("invalid NMEA sentence length") - errInvalidNMEAChecksum = errors.New("invalid NMEA sentence checksum") - errEmptyNMEASentence = errors.New("cannot parse empty NMEA sentence") - errUnknownNMEASentence = errors.New("unsupported NMEA sentence type") + ErrInvalidNMEASentenceLength = errors.New("invalid NMEA sentence length") + ErrInvalidNMEASentence = errors.New("invalid NMEA sentence format") + ErrEmptyNMEASentence = errors.New("cannot parse empty NMEA sentence") + ErrUnknownNMEASentence = errors.New("unsupported NMEA sentence type") errInvalidGGASentence = errors.New("invalid GGA NMEA sentence") errInvalidRMCSentence = errors.New("invalid RMC NMEA sentence") errInvalidGLLSentence = errors.New("invalid GLL NMEA sentence") errGPSCommandRejected = errors.New("GPS command rejected (NAK)") errNoACKToGPSCommand = errors.New("no ACK to GPS command") - errInvalidNMEASentanceFormat = errors.New("invalid NMEA sentence format") ) const ( @@ -151,15 +150,15 @@ func (gps *Device) WriteBytes(bytes []byte) { // It has to end with a '*' character following by a checksum. func validSentence(sentence string) error { if len(sentence) < minimumNMEALength || sentence[0] != startingDelimiter || sentence[len(sentence)-3] != checksumDelimiter { - return errInvalidNMEASentenceLength + return ErrInvalidNMEASentenceLength } var cs byte = 0 for i := 1; i < len(sentence)-3; i++ { cs ^= sentence[i] } checksum := strings.ToUpper(hex.EncodeToString([]byte{cs})) - if checksum != sentence[len(sentence)-2:len(sentence)] { - return errInvalidNMEASentanceFormat + if checksum != sentence[len(sentence)-2:] { + return ErrInvalidNMEASentence } return nil diff --git a/gps/gpsparser.go b/gps/gpsparser.go index 8434587..8419b29 100644 --- a/gps/gpsparser.go +++ b/gps/gpsparser.go @@ -46,10 +46,10 @@ func NewParser() Parser { func (parser *Parser) Parse(sentence string) (Fix, error) { var fix Fix if sentence == "" { - return fix, errEmptyNMEASentence + return fix, ErrEmptyNMEASentence } if len(sentence) < 6 { - return fix, errInvalidNMEASentenceLength + return fix, ErrInvalidNMEASentenceLength } typ := sentence[3:6] switch typ { @@ -104,7 +104,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) { return fix, nil } - return fix, errInvalidNMEASentanceFormat + return fix, ErrUnknownNMEASentence } // findTime returns the time from an NMEA sentence: From 0e2fb829efb1498df1d02453ec1c18b25df3217d Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 11 Jan 2026 16:08:13 +0100 Subject: [PATCH 116/137] gps: improvements and corrections for config commands This contains some improvements and corrections for the gps driver It adds some additional functions for different modes (automobile, bike, etc) and also ignores the return results from any config commands. Basically due to the fact that there is a constant stream of updates coming from NMEA messages, the results from sending any UBX commands are getting lost. Better to just ignore them for now. Signed-off-by: deadprogram --- examples/gps/uart/main.go | 5 ++- gps/gps.go | 1 + gps/gpsparser.go | 36 ++++++++++++++++++++ gps/gpsparser_test.go | 18 +++++++++- gps/ublox.go | 71 ++++++++++++++++++++++++++++++--------- gps/ublox_test.go | 40 +++++++++++----------- 6 files changed, 133 insertions(+), 38 deletions(-) diff --git a/examples/gps/uart/main.go b/examples/gps/uart/main.go index c5309bf..228aa1a 100644 --- a/examples/gps/uart/main.go +++ b/examples/gps/uart/main.go @@ -52,7 +52,10 @@ func main() { } println() } else { - println("Waiting for fix...") + if fix.Type == gps.GSV { + // GSV sentence provides satellite count even if no fix yet + println(fix.Satellites, "satellites visible") + } } time.Sleep(200 * time.Millisecond) } diff --git a/gps/gps.go b/gps/gps.go index b064bbe..778c692 100644 --- a/gps/gps.go +++ b/gps/gps.go @@ -15,6 +15,7 @@ var ( ErrInvalidNMEASentence = errors.New("invalid NMEA sentence format") ErrEmptyNMEASentence = errors.New("cannot parse empty NMEA sentence") ErrUnknownNMEASentence = errors.New("unsupported NMEA sentence type") + errInvalidGSVSentence = errors.New("invalid GSV NMEA sentence") errInvalidGGASentence = errors.New("invalid GGA NMEA sentence") errInvalidRMCSentence = errors.New("invalid RMC NMEA sentence") errInvalidGLLSentence = errors.New("invalid GLL NMEA sentence") diff --git a/gps/gpsparser.go b/gps/gpsparser.go index 8419b29..cae739e 100644 --- a/gps/gpsparser.go +++ b/gps/gpsparser.go @@ -6,12 +6,28 @@ import ( "time" ) +type NMEASentenceType string + +const ( + GSA NMEASentenceType = "GSA" + GGA NMEASentenceType = "GGA" + GLL NMEASentenceType = "GLL" + GSV NMEASentenceType = "GSV" + RMC NMEASentenceType = "RMC" + VTG NMEASentenceType = "VTG" + ZDA NMEASentenceType = "ZDA" + TXT NMEASentenceType = "TXT" +) + // Parser for GPS NMEA sentences. type Parser struct { } // Fix is a GPS location fix type Fix struct { + // Type is the NMEA sentence type that provided this fix. + Type NMEASentenceType + // Valid if the fix was valid. Valid bool @@ -53,6 +69,23 @@ func (parser *Parser) Parse(sentence string) (Fix, error) { } typ := sentence[3:6] switch typ { + case "GSV": + // https://docs.novatel.com/OEM7/Content/Logs/GPGSV.htm + fields := strings.Split(sentence, ",") + // GSV sentences have at least 4 fields, but typically 8, 12, 16, or 20 depending on satellites in view + if len(fields) < 4 { + return fix, errInvalidGSVSentence + } + + fix.Type = GSV + + // Number of satellites in view is always field 3 + fix.Satellites = findSatellites(fields[3]) + + // GSV does not provide position, time, or fix validity + fix.Valid = false + + return fix, nil case "GGA": // https://docs.novatel.com/OEM7/Content/Logs/GPGGA.htm fields := strings.Split(sentence, ",") @@ -60,6 +93,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) { return fix, errInvalidGGASentence } + fix.Type = GGA fix.Time = findTime(fields[1]) fix.Latitude = findLatitude(fields[2], fields[3]) fix.Longitude = findLongitude(fields[4], fields[5]) @@ -75,6 +109,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) { return fix, errInvalidGLLSentence } + fix.Type = GLL fix.Latitude = findLatitude(fields[1], fields[2]) fix.Longitude = findLongitude(fields[3], fields[4]) fix.Time = findTime(fields[5]) @@ -89,6 +124,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) { return fix, errInvalidRMCSentence } + fix.Type = RMC fix.Time = findTime(fields[1]) fix.Valid = (fields[2] == "A") fix.Latitude = findLatitude(fields[3], fields[4]) diff --git a/gps/gpsparser_test.go b/gps/gpsparser_test.go index 8e035aa..0c1b56a 100644 --- a/gps/gpsparser_test.go +++ b/gps/gpsparser_test.go @@ -10,13 +10,29 @@ import ( func TestParseUnknownSentence(t *testing.T) { p := NewParser() - val := "$GPGSV,3,1,09,07,14,317,22,08,31,284,25,10,32,133,39,16,85,232,29*7F" + val := "$GPVTG,89.68,T,,M,0.00,N,0.0,K*5F" _, err := p.Parse(val) if err == nil { t.Error("should have unknown sentence err") } } +func TestParseGSV(t *testing.T) { + c := qt.New(t) + + p := NewParser() + + val := "$GPGSV,3,1,09,07,14,317,22,08,31,284,25,10,32,133,39,16,85,232,29*7F" + fix, err := p.Parse(val) + if err != nil { + t.Error("should have parsed") + } + + c.Assert(fix.Type, qt.Equals, GSV) + c.Assert(fix.Satellites, qt.Equals, int16(9)) + c.Assert(fix.Valid, qt.Equals, false) +} + func TestParseGGA(t *testing.T) { c := qt.New(t) diff --git a/gps/ublox.go b/gps/ublox.go index ce6f020..4167715 100644 --- a/gps/ublox.go +++ b/gps/ublox.go @@ -4,9 +4,8 @@ import ( "time" ) -// FlightModeCmd is a UBX-CFG-NAV5 command to set the GPS into -// flight mode (airborne <1g) -var flightModeCmd = CfgNav5{ +// FlightModeCmd is a UBX-CFG-NAV5 command +var nav5Cmd = CfgNav5{ Mask: CfgNav5Dyn | CfgNav5MinEl | CfgNav5PosFixMode, DynModel: DynModeAirborne1g, // Airborne with <1g acceleration FixMode: FixModeAuto, // Auto 2D/3D @@ -29,7 +28,37 @@ var flightModeCmd = CfgNav5{ // SetFlightMode sends UBX-CFG-NAV5 command to set GPS into flight mode func (d *Device) SetFlightMode() (err error) { - flightModeCmd.Put42Bytes(d.buffer[:]) + nav5Cmd.DynModel = DynModeAirborne1g + nav5Cmd.FixMode = FixModeAuto + nav5Cmd.Put42Bytes(d.buffer[:]) + + return d.SendCommand(d.buffer[:42]) +} + +// SetPedestrianMode sends UBX-CFG-NAV5 command to set GPS into pedestrian mode +func (d *Device) SetPedestrianMode() (err error) { + nav5Cmd.DynModel = DynModePedestrian + nav5Cmd.FixMode = FixModeAuto + nav5Cmd.Put42Bytes(d.buffer[:]) + + return d.SendCommand(d.buffer[:42]) +} + +// SetAutomotiveMode sends UBX-CFG-NAV5 command to set GPS into automotive mode +func (d *Device) SetAutomotiveMode() (err error) { + nav5Cmd.DynModel = DynModeAutomotive + nav5Cmd.FixMode = FixModeAuto + nav5Cmd.Put42Bytes(d.buffer[:]) + + return d.SendCommand(d.buffer[:42]) +} + +// SetBikeMode sends UBX-CFG-NAV5 command to set GPS into bike mode +func (d *Device) SetBikeMode() (err error) { + nav5Cmd.DynModel = DynModeBike + nav5Cmd.FixMode = FixModeAuto + nav5Cmd.Put42Bytes(d.buffer[:]) + return d.SendCommand(d.buffer[:42]) } @@ -44,19 +73,19 @@ var ( messageRateGLLCmd = CfgMsg1{ MsgClass: 0xF0, MsgID: 0x01, - Rate: 0, // Disabled + Rate: 1, // Every position fix } // GSA (satellite id list) messageRateGSACmd = CfgMsg1{ MsgClass: 0xF0, MsgID: 0x02, - Rate: 1, // Every position fix + Rate: 0, // Disabled } // GSV (satellite locations) messageRateGSVCmd = CfgMsg1{ MsgClass: 0xF0, MsgID: 0x03, - Rate: 1, // Every position fix + Rate: 0, // Every position fix } // RMC (time, lat/lng, speed, course) messageRateRMCCmd = CfgMsg1{ @@ -84,18 +113,19 @@ var ( } ) -// SetMessageRatesMinimal configures the GPS to output a minimal set of NMEA sentences +// SetMessageRatesMinimal configures the GPS to output a minimal set of NMEA sentences: +// GSV, GGA, GLL, and RMC only. func SetMessageRatesMinimal(d *Device) (err error) { commands := []CfgMsg1{ messageRateGSACmd, - messageRateGGACmd, messageRateGLLCmd, - messageRateGSVCmd, - messageRateRMCCmd, messageRateVTGCmd, messageRateZDACmd, messageRateTXTCmd, } + for i := range commands { + commands[i].Rate = 0 // Disable + } return setCfg1s(d, commands) } @@ -111,18 +141,26 @@ func SetMessageRatesAllEnabled(d *Device) (err error) { messageRateZDACmd, messageRateTXTCmd, } + for i := range commands { + commands[i].Rate = 1 // Enable + } return setCfg1s(d, commands) } func setCfg1s(d *Device, commands []CfgMsg1) (err error) { var buf [9]byte for _, cmd := range commands { - cmd.Put9Bytes(buf[:9]) - if err = d.SendCommand(buf[:9]); err != nil { - return err - } + cmd.Put9Bytes(buf[:]) + // TODO handle errors differently here? + // This implementation just saves the last error and continues. + // Due to the GPS modules sending updates asynchronously + // the response is interleaved along with regular ASCII + // NMEA messages. + err = d.SendCommand(buf[:]) + time.Sleep(100 * time.Millisecond) } - return nil + + return } // gnssDisableCmd is a UBX-CFG-GNSS command to disable all GNSS but GPS @@ -146,6 +184,7 @@ func (d *Device) SetGNSSDisable() (err error) { if err != nil { return err } + return d.SendCommand(d.buffer[:]) } diff --git a/gps/ublox_test.go b/gps/ublox_test.go index 1101e89..4bad0c2 100644 --- a/gps/ublox_test.go +++ b/gps/ublox_test.go @@ -65,23 +65,23 @@ func TestAppendChecksumPreservesOriginal(t *testing.T) { } } -func TestFlightModeCmdConfig(t *testing.T) { - // Verify FlightModeCmd has expected values - if flightModeCmd.DynModel != 6 { - t.Errorf("expected DynModel 6 (airborne <1g), got %d", flightModeCmd.DynModel) +func TestNav5CmdConfig(t *testing.T) { + // Verify nav5Cmd has expected values + if nav5Cmd.DynModel != 6 { + t.Errorf("expected DynModel 6 (airborne <1g), got %d", nav5Cmd.DynModel) } - if flightModeCmd.FixMode != 3 { - t.Errorf("expected FixMode 3 (auto 2D/3D), got %d", flightModeCmd.FixMode) + if nav5Cmd.FixMode != 3 { + t.Errorf("expected FixMode 3 (auto 2D/3D), got %d", nav5Cmd.FixMode) } expectedMask := CfgNav5Dyn | CfgNav5MinEl | CfgNav5PosFixMode - if flightModeCmd.Mask != expectedMask { - t.Errorf("expected Mask 0x%04X, got 0x%04X", expectedMask, flightModeCmd.Mask) + if nav5Cmd.Mask != expectedMask { + t.Errorf("expected Mask 0x%04X, got 0x%04X", expectedMask, nav5Cmd.Mask) } - if flightModeCmd.MinElev_deg != 5 { - t.Errorf("expected MinElev_deg 5, got %d", flightModeCmd.MinElev_deg) + if nav5Cmd.MinElev_deg != 5 { + t.Errorf("expected MinElev_deg 5, got %d", nav5Cmd.MinElev_deg) } } @@ -118,9 +118,9 @@ func TestGNSSDisableCmdConfig(t *testing.T) { } } -func TestFlightModeCmdWrite(t *testing.T) { +func TestNav5CmdWrite(t *testing.T) { buf := make([]byte, 64) - flightModeCmd.Put42Bytes(buf) + nav5Cmd.Put42Bytes(buf) // Verify sync chars if buf[0] != 0xB5 || buf[1] != 0x62 { @@ -197,9 +197,9 @@ func TestMessageRateCmdConfigs(t *testing.T) { rate byte }{ {"GGA", messageRateGGACmd, 0xF0, 0x00, 1}, - {"GLL", messageRateGLLCmd, 0xF0, 0x01, 0}, - {"GSA", messageRateGSACmd, 0xF0, 0x02, 1}, - {"GSV", messageRateGSVCmd, 0xF0, 0x03, 1}, + {"GLL", messageRateGLLCmd, 0xF0, 0x01, 1}, + {"GSA", messageRateGSACmd, 0xF0, 0x02, 0}, + {"GSV", messageRateGSVCmd, 0xF0, 0x03, 0}, {"RMC", messageRateRMCCmd, 0xF0, 0x04, 1}, {"VTG", messageRateVTGCmd, 0xF0, 0x05, 0}, {"ZDA", messageRateZDACmd, 0xF0, 0x08, 0}, @@ -269,9 +269,9 @@ func TestMinimalMessageRatesConfig(t *testing.T) { // GGA and RMC should be enabled (rate=1), others disabled (rate=0) expectedRates := map[byte]byte{ 0x00: 1, // GGA - enabled - 0x01: 0, // GLL - disabled - 0x02: 1, // GSA - enabled - 0x03: 1, // GSV - enabled + 0x01: 1, // GLL - enabled + 0x02: 0, // GSA - disabled + 0x03: 0, // GSV - disabled 0x04: 1, // RMC - enabled 0x05: 0, // VTG - disabled 0x08: 0, // ZDA - disabled @@ -337,9 +337,9 @@ func TestAllMessageRatesWriteCorrectBytes(t *testing.T) { func TestSetMessageRatesAllEnabledModifiesRate(t *testing.T) { // Verify that when we copy a command and set Rate=1, it works correctly - cmd := messageRateGLLCmd // This one is disabled by default + cmd := messageRateGSACmd // This one is disabled by default if cmd.Rate != 0 { - t.Errorf("expected GLL default rate 0, got %d", cmd.Rate) + t.Errorf("expected GSA default rate 0, got %d", cmd.Rate) } // Simulate what SetMessageRatesAllEnabled does From d6114c9f6deeb86a4407fdb14da0acc9f687021e Mon Sep 17 00:00:00 2001 From: deadprogram Date: Thu, 12 Feb 2026 12:52:11 +0100 Subject: [PATCH 117/137] sponsorship: add explicit callout/link in README to help out TinyGo Signed-off-by: deadprogram --- .github/FUNDING.yml | 1 + README.md | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 016f657..869e436 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,2 +1,3 @@ # These are supported funding model platforms + open_collective: tinygo diff --git a/README.md b/README.md index eba41d7..9cff324 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,9 @@ This package provides a collection of over 130 different hardware drivers for de For the complete list, please see: https://tinygo.org/docs/reference/devices/ +> [!IMPORTANT] +> You can help TinyGo with a financial contribution using OpenCollective. Please see https://opencollective.com/tinygo for more information. Thank you! + ## Installing ```shell From 9eb95a465137f45a1624d8a23d6cbe0567307686 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sat, 21 Feb 2026 09:40:46 +0100 Subject: [PATCH 118/137] make: add tasks for counting the number of drivers, and displaying a list of them Signed-off-by: deadprogram --- Makefile | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Makefile b/Makefile index c05ef64..0a6915e 100644 --- a/Makefile +++ b/Makefile @@ -26,3 +26,17 @@ unit-test: @go test -v $(addprefix ./,$(TESTS)) test: clean fmt-check unit-test smoke-test + +EXCLUDE_DIRS = build cmd examples internal lora ndir netdev netlink tester + +drivers-count: + @root_count=$$(find . -mindepth 1 -maxdepth 1 -type d | grep -vE '^\./($(subst $(space),|,$(EXCLUDE_DIRS)))$$' | wc -l); \ + epd_count=$$(find ./waveshare-epd -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l); \ + total=$$((root_count + epd_count)); \ + echo "Total drivers: $$total (root: $$root_count, waveshare-epd: $$epd_count)" + +drivers-list: + @{ \ + find . -mindepth 1 -maxdepth 1 -type d | grep -vE '^\./($(subst $(space),|,$(EXCLUDE_DIRS)))$$'; \ + if [ -d ./waveshare-epd ]; then find ./waveshare-epd -mindepth 1 -maxdepth 1 -type d; fi; \ + } | sed 's|^\./||' | sort From b480978e1a27c5390b6f746dbb668a7f84aa2213 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sat, 21 Feb 2026 09:41:11 +0100 Subject: [PATCH 119/137] docs: update count of drivers Signed-off-by: deadprogram --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9cff324..d049163 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PkgGoDev](https://pkg.go.dev/badge/tinygo.org/x/drivers)](https://pkg.go.dev/tinygo.org/x/drivers) [![Build](https://github.com/tinygo-org/drivers/actions/workflows/build.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/drivers/actions/workflows/build.yml) -This package provides a collection of over 130 different hardware drivers for devices such as sensors, displays, wireless adaptors, and actuators, that can be used together with [TinyGo](https://tinygo.org). +This package provides a collection of 139 different hardware drivers for devices such as sensors, displays, wireless adaptors, and actuators, that can be used together with [TinyGo](https://tinygo.org). For the complete list, please see: https://tinygo.org/docs/reference/devices/ From a514169c3727194c9d2d14a578a43f4209caf377 Mon Sep 17 00:00:00 2001 From: Daniel Esteban Date: Wed, 25 Mar 2026 00:22:15 +0100 Subject: [PATCH 120/137] =?UTF-8?q?fix=20ST7789=20driver=20when=20rotated?= =?UTF-8?q?=2090=C2=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- st7789/st7789.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/st7789/st7789.go b/st7789/st7789.go index 5db2402..be28a60 100644 --- a/st7789/st7789.go +++ b/st7789/st7789.go @@ -513,8 +513,8 @@ func (d *DeviceOf[T]) setRotation(rotation Rotation) error { d.columnOffset = 0 case drivers.Rotation90: madctl = MADCTL_MX | MADCTL_MV - d.rowOffset = 0 - d.columnOffset = 0 + d.rowOffset = d.columnOffsetCfg + d.columnOffset = d.rowOffsetCfg case drivers.Rotation180: madctl = MADCTL_MX | MADCTL_MY d.rowOffset = d.rowOffsetCfg From 4071028e855deaf215df1ed3c63e5f025186b446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Henrique=20Guard=C3=A3o=20Gandarez?= Date: Tue, 31 Mar 2026 19:26:58 -0300 Subject: [PATCH 121/137] ws2812: add PIO support for RP2040/RP2350 Integrate PIO support directly into the existing Device. NewWS2812() now uses PIO for hardware-timed control on RP2040/RP2350 and falls back to bit-banging on other platforms. No changes to the exported API surface. --- go.mod | 3 +-- go.sum | 2 ++ ws2812/ws2812.go | 14 +++++++------- ws2812/ws2812_init_other.go | 10 ++++++++++ ws2812/ws2812_rp2_pio.go | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 ws2812/ws2812_init_other.go create mode 100644 ws2812/ws2812_rp2_pio.go diff --git a/go.mod b/go.mod index 10c3e98..432c77a 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,16 @@ module tinygo.org/x/drivers - go 1.22.1 toolchain go1.23.1 - require ( github.com/eclipse/paho.mqtt.golang v1.2.0 github.com/frankban/quicktest v1.10.2 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/orsinium-labs/tinymath v1.1.0 github.com/soypat/natiu-mqtt v0.5.1 + github.com/tinygo-org/pio v0.3.0 golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d golang.org/x/net v0.33.0 tinygo.org/x/tinyfont v0.3.0 diff --git a/go.sum b/go.sum index 6bb3557..673d984 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/orsinium-labs/tinymath v1.1.0 h1:KomdsyLHB7vE3f1nRAJF2dyf1m/gnM2HxfTe github.com/orsinium-labs/tinymath v1.1.0/go.mod h1:WPXX6ei3KSXG7JfA03a+ekCYaY9SWN4I+JRl2p6ck+A= github.com/soypat/natiu-mqtt v0.5.1 h1:rwaDmlvjzD2+3MCOjMZc4QEkDkNwDzbct2TJbpz+TPc= github.com/soypat/natiu-mqtt v0.5.1/go.mod h1:xEta+cwop9izVCW7xOx2W+ct9PRMqr0gNVkvBPnQTc4= +github.com/tinygo-org/pio v0.3.0 h1:opEnOtw58KGB4RJD3/n/Rd0/djYGX3DeJiXLI6y/yDI= +github.com/tinygo-org/pio v0.3.0/go.mod h1:wf6c6lKZp+pQOzKKcpzchmRuhiMc27ABRuo7KVnaMFU= github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= diff --git a/ws2812/ws2812.go b/ws2812/ws2812.go index dee3cfb..583cbbd 100644 --- a/ws2812/ws2812.go +++ b/ws2812/ws2812.go @@ -1,4 +1,7 @@ // Package ws2812 implements a driver for WS2812 and SK6812 RGB LED strips. +// +// On most platforms NewWS2812 uses bit-banging. +// On RP2040/RP2350 it uses PIO for hardware-timed control. package ws2812 // import "tinygo.org/x/drivers/ws2812" //go:generate go run gen-ws2812.go -arch=cortexm 16 48 64 120 125 150 168 200 @@ -24,14 +27,11 @@ func New(pin machine.Pin) Device { return NewWS2812(pin) } -// New returns a new WS2812(RGB) driver. -// It does not touch the pin object: you have -// to configure it as an output pin before calling New. +// NewWS2812 returns a new WS2812(RGB) driver. +// On RP2040/RP2350, it uses PIO for hardware-timed control. +// On other platforms, you must configure the pin as output before calling this. func NewWS2812(pin machine.Pin) Device { - return Device{ - Pin: pin, - writeColorFunc: writeColorsRGB, - } + return newWS2812Device(pin) } // New returns a new SK6812(RGBA) driver. diff --git a/ws2812/ws2812_init_other.go b/ws2812/ws2812_init_other.go new file mode 100644 index 0000000..8d312de --- /dev/null +++ b/ws2812/ws2812_init_other.go @@ -0,0 +1,10 @@ +//go:build !rp2040 && !rp2350 + +package ws2812 + +import "machine" + +// newWS2812Device creates a WS2812 device using the bit-bang driver. +func newWS2812Device(pin machine.Pin) Device { + return Device{Pin: pin, writeColorFunc: writeColorsRGB} +} diff --git a/ws2812/ws2812_rp2_pio.go b/ws2812/ws2812_rp2_pio.go new file mode 100644 index 0000000..36259e5 --- /dev/null +++ b/ws2812/ws2812_rp2_pio.go @@ -0,0 +1,33 @@ +//go:build rp2040 || rp2350 + +package ws2812 + +import ( + "image/color" + "machine" + + pio "github.com/tinygo-org/pio/rp2-pio" + "github.com/tinygo-org/pio/rp2-pio/piolib" +) + +// newWS2812Device creates a WS2812 device using PIO for hardware-timed control. +// If PIO initialization fails, it falls back to the bit-bang driver. +func newWS2812Device(pin machine.Pin) Device { + sm, err := pio.PIO0.ClaimStateMachine() + if err != nil { + return Device{Pin: pin, writeColorFunc: writeColorsRGB} + } + ws, err := piolib.NewWS2812B(sm, pin) + if err != nil { + return Device{Pin: pin, writeColorFunc: writeColorsRGB} + } + return Device{ + Pin: pin, + writeColorFunc: func(_ Device, buf []color.RGBA) error { + for _, c := range buf { + ws.PutRGB(c.R, c.G, c.B) + } + return nil + }, + } +} From e232a4f136d54223dea36b842cb90632de34a043 Mon Sep 17 00:00:00 2001 From: Avinal Kumar Date: Sun, 29 Mar 2026 19:02:13 +0530 Subject: [PATCH 122/137] waveshare(ssd1680): Add driver for waveshare 2.9 inch v2 epaper - Waveshare 2.9in v2 epaper uses a different IC (SSD1680) than the v1. This commit takes the code from v1 and modifies them for v2. - Datasheets: - https://files.waveshare.com/upload/7/79/2.9inch-e-paper-v2-specification.pdf - https://cdn-learn.adafruit.com/assets/assets/000/097/631/original/SSD1680_Datasheet.pdf?1607625960 - Code reference: https://github.com/waveshareteam/e-Paper/raw/refs/heads/master/RaspberryPi_JetsonNano/c/lib/e-Paper/EPD_2in9_V2.c - Fixes #627 Signed-off-by: Avinal Kumar Assisted-by: Claude Code --- examples/waveshare-epd/epd2in9v2/main.go | 137 +++++++ smoketest.sh | 1 + waveshare-epd/epd2in9v2/epd2in9v2.go | 465 +++++++++++++++++++++++ waveshare-epd/epd2in9v2/registers.go | 50 +++ 4 files changed, 653 insertions(+) create mode 100644 examples/waveshare-epd/epd2in9v2/main.go create mode 100644 waveshare-epd/epd2in9v2/epd2in9v2.go create mode 100644 waveshare-epd/epd2in9v2/registers.go diff --git a/examples/waveshare-epd/epd2in9v2/main.go b/examples/waveshare-epd/epd2in9v2/main.go new file mode 100644 index 0000000..3bbbd57 --- /dev/null +++ b/examples/waveshare-epd/epd2in9v2/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "image/color" + "machine" + "time" + + "tinygo.org/x/drivers/waveshare-epd/epd2in9v2" +) + +var display epd2in9v2.Device + +func main() { + machine.SPI0.Configure(machine.SPIConfig{ + Frequency: 12000000, + SCK: machine.EPD_SCK_PIN, + SDO: machine.EPD_SDO_PIN, + }) + + display = epd2in9v2.New( + machine.SPI0, + machine.EPD_CS_PIN, + machine.EPD_DC_PIN, + machine.EPD_RESET_PIN, + machine.EPD_BUSY_PIN, + ) + display.Configure(epd2in9v2.Config{ + Rotation: epd2in9v2.ROTATION_270, + Speed: epd2in9v2.SPEED_DEFAULT, + Blocking: true, + }) + + black := color.RGBA{0, 0, 0, 255} + white := color.RGBA{255, 255, 255, 255} + + // --- Step 1: clear to white --- + println("epd2in9v2: clearing display") + display.ClearBuffer() + display.Display() + time.Sleep(2 * time.Second) + + // --- Step 2: full refresh checkerboard --- + println("epd2in9v2: drawing checkerboard (full refresh)") + w, h := display.Size() + for i := int16(0); i < w/8; i++ { + for j := int16(0); j < h/8; j++ { + if (i+j)%2 == 0 { + fillRect(i*8, j*8, 8, 8, black) + } + } + } + display.Display() + time.Sleep(2 * time.Second) + + // --- Step 3: fast refresh - draw border and diagonal cross --- + println("epd2in9v2: switching to fast refresh") + display.SetSpeed(epd2in9v2.SPEED_FAST) + display.ClearBuffer() + + for x := int16(0); x < w; x++ { + display.SetPixel(x, 0, black) + display.SetPixel(x, h-1, black) + } + for y := int16(0); y < h; y++ { + display.SetPixel(0, y, black) + display.SetPixel(w-1, y, black) + } + for i := int16(0); i < w && i < h; i++ { + display.SetPixel(i, i*h/w, black) + display.SetPixel(w-1-i, i*h/w, black) + } + + display.Display() + time.Sleep(2 * time.Second) + + // --- Step 4: partial refresh counter --- + println("epd2in9v2: partial refresh demo") + display.SetSpeed(epd2in9v2.SPEED_DEFAULT) + display.ClearBuffer() + + println("epd2in9v2: setting base image") + display.DisplayWithBase() + + for count := 0; count < 10; count++ { + cx := int16(120) + cy := int16(50) + fillRect(cx, cy, 60, 20, white) + + digit := int16(count % 10) + drawDigit(cx+22, cy+2, digit, black) + + display.DisplayPartial() + time.Sleep(500 * time.Millisecond) + } + time.Sleep(2 * time.Second) + + // --- Step 5: sleep --- + println("epd2in9v2: entering deep sleep") + display.ClearBuffer() + display.Display() + display.Sleep() + println("epd2in9v2: done, you can remove power") +} + +func fillRect(x, y, w, h int16, c color.RGBA) { + for i := x; i < x+w; i++ { + for j := y; j < y+h; j++ { + display.SetPixel(i, j, c) + } + } +} + +// drawDigit draws a simple 3x5-pixel-block digit (each block 4x3 px) at position (x,y). +func drawDigit(x, y, digit int16, c color.RGBA) { + segments := [10][5]uint8{ + {0x7, 0x5, 0x5, 0x5, 0x7}, // 0 + {0x2, 0x2, 0x2, 0x2, 0x2}, // 1 + {0x7, 0x1, 0x7, 0x4, 0x7}, // 2 + {0x7, 0x1, 0x7, 0x1, 0x7}, // 3 + {0x5, 0x5, 0x7, 0x1, 0x1}, // 4 + {0x7, 0x4, 0x7, 0x1, 0x7}, // 5 + {0x7, 0x4, 0x7, 0x5, 0x7}, // 6 + {0x7, 0x1, 0x1, 0x1, 0x1}, // 7 + {0x7, 0x5, 0x7, 0x5, 0x7}, // 8 + {0x7, 0x5, 0x7, 0x1, 0x7}, // 9 + } + if digit < 0 || digit > 9 { + return + } + for row := int16(0); row < 5; row++ { + for col := int16(0); col < 3; col++ { + if segments[digit][row]&(0x4>>uint(col)) != 0 { + fillRect(x+col*4, y+row*3, 4, 3, c) + } + } + } +} diff --git a/smoketest.sh b/smoketest.sh index d25133d..75316bb 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -128,6 +128,7 @@ tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/sx127x/l tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/ssd1289/main.go tinygo build -size short -o ./build/test.hex -target=pico ./examples/irremote/main.go tinygo build -size short -o ./build/test.hex -target=badger2040 ./examples/uc8151/main.go +tinygo build -size short -o ./build/test.hex -target=badger2040 ./examples/waveshare-epd/epd2in9v2/main.go tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/scd4x/main.go tinygo build -size short -o ./build/test.uf2 -target=circuitplay-express ./examples/makeybutton/main.go tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/ds18b20/main.go diff --git a/waveshare-epd/epd2in9v2/epd2in9v2.go b/waveshare-epd/epd2in9v2/epd2in9v2.go new file mode 100644 index 0000000..d6493a2 --- /dev/null +++ b/waveshare-epd/epd2in9v2/epd2in9v2.go @@ -0,0 +1,465 @@ +// Package epd2in9v2 implements a driver for the Waveshare 2.9in V2 black and white e-paper display. +// +// This is for the V2 device using the SSD1680 chipset. For the V1 device (using IL3820), +// use the epd2in9 package instead. +// +// Datasheet: +// https://files.waveshare.com/upload/7/79/2.9inch-e-paper-v2-specification.pdf +// https://cdn-learn.adafruit.com/assets/assets/000/097/631/original/SSD1680_Datasheet.pdf?1607625960 +// +// Reference: https://github.com/waveshareteam/e-Paper/tree/master/RaspberryPi_JetsonNano/c/lib/e-Paper +package epd2in9v2 // import "tinygo.org/x/drivers/waveshare-epd/epd2in9v2" + +import ( + "image/color" + "machine" + "time" + + "tinygo.org/x/drivers" +) + +type Config struct { + Width int16 + Height int16 + Rotation Rotation + Speed Speed + Blocking bool +} + +type Device struct { + bus drivers.SPI + cs machine.Pin + dc machine.Pin + rst machine.Pin + busy machine.Pin + width int16 + height int16 + buffer []uint8 + bufferLength uint32 + rotation Rotation + speed Speed + blocking bool +} + +type Rotation uint8 +type Speed uint8 + +// LUT for normal full refresh (~2s) +var lutDefault = [159]uint8{ + 0x80, 0x66, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, + 0x10, 0x66, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, + 0x80, 0x66, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, + 0x10, 0x66, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x14, 0x8, 0x0, 0x0, 0x0, 0x0, 0x1, + 0xA, 0xA, 0x0, 0xA, 0xA, 0x0, 0x1, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x14, 0x8, 0x0, 0x1, 0x0, 0x0, 0x1, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x0, 0x0, 0x0, + 0x22, 0x17, 0x41, 0x0, 0x32, 0x36, +} + +// LUT for fast full refresh (~1s) +var lutFast = [159]uint8{ + 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x19, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x24, 0x42, 0x22, 0x22, 0x23, 0x32, 0x00, 0x00, 0x00, + 0x22, 0x17, 0x41, 0xAE, 0x32, 0x38, +} + +// LUT for partial refresh +var lutPartial = [159]uint8{ + 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0A, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, + 0x22, 0x17, 0x41, 0xB0, 0x32, 0x36, +} + +// New returns a new epd2in9v2 driver. Pass in a fully configured SPI bus. +func New(bus drivers.SPI, csPin, dcPin, rstPin, busyPin machine.Pin) Device { + csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + rstPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + busyPin.Configure(machine.PinConfig{Mode: machine.PinInput}) + return Device{ + bus: bus, + cs: csPin, + dc: dcPin, + rst: rstPin, + busy: busyPin, + } +} + +// Configure sets up the device. +func (d *Device) Configure(cfg Config) { + if cfg.Width != 0 { + d.width = cfg.Width + } else { + d.width = EPD_WIDTH + } + if cfg.Height != 0 { + d.height = cfg.Height + } else { + d.height = EPD_HEIGHT + } + d.rotation = cfg.Rotation + d.speed = cfg.Speed + d.blocking = cfg.Blocking + d.bufferLength = (uint32(d.width) * uint32(d.height)) / 8 + d.buffer = make([]uint8, d.bufferLength) + for i := uint32(0); i < d.bufferLength; i++ { + d.buffer[i] = 0xFF + } + + d.Reset() + time.Sleep(100 * time.Millisecond) + + d.WaitUntilIdle() + d.SendCommand(SW_RESET) + d.WaitUntilIdle() + + d.SendCommand(DRIVER_OUTPUT_CONTROL) + d.SendData(uint8((d.height - 1) & 0xFF)) + d.SendData(uint8((d.height - 1) >> 8)) + d.SendData(0x00) + + d.SendCommand(DATA_ENTRY_MODE) + d.SendData(0x03) + + d.setWindow(0, 0, d.width-1, d.height-1) + + if cfg.Speed == SPEED_FAST { + d.SendCommand(BORDER_WAVEFORM_CONTROL) + d.SendData(0x05) + } + + d.SendCommand(DISPLAY_UPDATE_CONTROL_1) + d.SendData(0x00) + d.SendData(0x80) + + d.setCursor(0, 0) + d.WaitUntilIdle() + + switch cfg.Speed { + case SPEED_FAST: + d.setLUTByHost(&lutFast) + default: + d.setLUTByHost(&lutDefault) + } +} + +// HardwareReset resets the device via the RST pin. +func (d *Device) Reset() { + d.rst.High() + time.Sleep(10 * time.Millisecond) + d.rst.Low() + time.Sleep(2 * time.Millisecond) + d.rst.High() + time.Sleep(10 * time.Millisecond) +} + +// SendCommand sends a command byte to the display. +func (d *Device) SendCommand(command uint8) { + d.dc.Low() + d.cs.Low() + d.bus.Transfer(command) + d.cs.High() +} + +// SendData sends a data byte to the display. +func (d *Device) SendData(data uint8) { + d.dc.High() + d.cs.Low() + d.bus.Transfer(data) + d.cs.High() +} + +// WaitUntilIdle waits until the display is ready. +// On SSD1680, BUSY pin is HIGH when busy, LOW when idle. +func (d *Device) WaitUntilIdle() { + for d.busy.Get() { + time.Sleep(50 * time.Millisecond) + } + time.Sleep(50 * time.Millisecond) +} + +// IsBusy returns the busy status of the display. +func (d *Device) IsBusy() bool { + return d.busy.Get() +} + +// SetPixel modifies the internal buffer in a single pixel. +// Uses color.RGBA where black (0,0,0) = black pixel, anything else = white pixel. +func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { + x, y = d.xy(x, y) + + if x < 0 || x >= d.width || y < 0 || y >= d.height { + return + } + byteIndex := (y * (d.width / 8)) + (x / 8) + if c.R == 0 && c.G == 0 && c.B == 0 { + d.buffer[byteIndex] &^= 0x80 >> uint8(x%8) + } else { + d.buffer[byteIndex] |= 0x80 >> uint8(x%8) + } +} + +// Display sends the buffer to the screen. +func (d *Device) Display() error { + if d.blocking { + d.WaitUntilIdle() + } + + d.setCursor(0, 0) + d.SendCommand(WRITE_RAM_BW) + for i := uint32(0); i < d.bufferLength; i++ { + d.SendData(d.buffer[i]) + } + + d.turnOnDisplay() + + if d.blocking { + d.WaitUntilIdle() + } + return nil +} + +// DisplayWithBase writes the buffer to both BW and RED RAM then refreshes. +// This is useful before partial updates to set the base image. +func (d *Device) DisplayWithBase() error { + if d.blocking { + d.WaitUntilIdle() + } + + d.setCursor(0, 0) + d.SendCommand(WRITE_RAM_BW) + for i := uint32(0); i < d.bufferLength; i++ { + d.SendData(d.buffer[i]) + } + + d.setCursor(0, 0) + d.SendCommand(WRITE_RAM_RED) + for i := uint32(0); i < d.bufferLength; i++ { + d.SendData(d.buffer[i]) + } + + d.turnOnDisplay() + + if d.blocking { + d.WaitUntilIdle() + } + return nil +} + +// DisplayPartial performs a partial refresh of the display. +// Call DisplayWithBase first to set the base image before using partial updates. +func (d *Device) DisplayPartial() error { + d.rst.Low() + time.Sleep(1 * time.Millisecond) + d.rst.High() + time.Sleep(2 * time.Millisecond) + + d.setLUT(&lutPartial) + + d.SendCommand(OTP_SELECTION_CONTROL) + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x40) + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x00) + + d.SendCommand(BORDER_WAVEFORM_CONTROL) + d.SendData(0x80) + + d.SendCommand(DISPLAY_UPDATE_CONTROL_2) + d.SendData(0xC0) + d.SendCommand(MASTER_ACTIVATION) + d.WaitUntilIdle() + + d.setWindow(0, 0, d.width-1, d.height-1) + d.setCursor(0, 0) + + d.SendCommand(WRITE_RAM_BW) + for i := uint32(0); i < d.bufferLength; i++ { + d.SendData(d.buffer[i]) + } + + d.turnOnDisplayPartial() + d.WaitUntilIdle() + return nil +} + +// ClearDisplay erases the display. +func (d *Device) ClearDisplay() { + d.ClearBuffer() + d.Display() +} + +// ClearBuffer sets the buffer to 0xFF (white). +func (d *Device) ClearBuffer() { + for i := uint32(0); i < d.bufferLength; i++ { + d.buffer[i] = 0xFF + } +} + +// Size returns the current size of the display. +func (d *Device) Size() (w, h int16) { + if d.rotation == ROTATION_90 || d.rotation == ROTATION_270 { + return d.height, d.width + } + return d.width, d.height +} + +// SetRotation changes the rotation (clock-wise) of the device. +func (d *Device) SetRotation(rotation Rotation) { + d.rotation = rotation +} + +// SetBlocking changes the blocking flag of the device. +func (d *Device) SetBlocking(blocking bool) { + d.blocking = blocking +} + +// SetSpeed changes the refresh speed and reconfigures the device. +func (d *Device) SetSpeed(speed Speed) { + d.Configure(Config{ + Width: d.width, + Height: d.height, + Rotation: d.rotation, + Speed: speed, + Blocking: d.blocking, + }) +} + +// Sleep puts the display into deep sleep mode. A hardware reset is needed to wake it. +func (d *Device) Sleep() { + d.SendCommand(DEEP_SLEEP_MODE) + d.SendData(0x01) + time.Sleep(100 * time.Millisecond) +} + +// PowerOff disables the display analog/clock. Lighter than Sleep. +func (d *Device) PowerOff() { + d.SendCommand(DISPLAY_UPDATE_CONTROL_2) + d.SendData(0x03) + d.SendCommand(MASTER_ACTIVATION) + d.WaitUntilIdle() +} + +func (d *Device) xy(x, y int16) (int16, int16) { + switch d.rotation { + case NO_ROTATION: + return x, y + case ROTATION_90: + return d.width - y - 1, x + case ROTATION_180: + return d.width - x - 1, d.height - y - 1 + case ROTATION_270: + return y, d.height - x - 1 + } + return x, y +} + +func (d *Device) setWindow(xStart, yStart, xEnd, yEnd int16) { + d.SendCommand(SET_RAM_X_ADDRESS) + d.SendData(uint8((xStart >> 3) & 0xFF)) + d.SendData(uint8((xEnd >> 3) & 0xFF)) + + d.SendCommand(SET_RAM_Y_ADDRESS) + d.SendData(uint8(yStart & 0xFF)) + d.SendData(uint8((yStart >> 8) & 0xFF)) + d.SendData(uint8(yEnd & 0xFF)) + d.SendData(uint8((yEnd >> 8) & 0xFF)) +} + +func (d *Device) setCursor(x, y int16) { + d.SendCommand(SET_RAM_X_COUNTER) + d.SendData(uint8(x & 0xFF)) + + d.SendCommand(SET_RAM_Y_COUNTER) + d.SendData(uint8(y & 0xFF)) + d.SendData(uint8((y >> 8) & 0xFF)) +} + +func (d *Device) turnOnDisplay() { + d.SendCommand(DISPLAY_UPDATE_CONTROL_2) + d.SendData(0xC7) + d.SendCommand(MASTER_ACTIVATION) + d.WaitUntilIdle() +} + +func (d *Device) turnOnDisplayPartial() { + d.SendCommand(DISPLAY_UPDATE_CONTROL_2) + d.SendData(0x0F) + d.SendCommand(MASTER_ACTIVATION) + d.WaitUntilIdle() +} + +func (d *Device) setLUT(lut *[159]uint8) { + d.SendCommand(WRITE_LUT_REGISTER) + for i := 0; i < 153; i++ { + d.SendData(lut[i]) + } + d.WaitUntilIdle() +} + +func (d *Device) setLUTByHost(lut *[159]uint8) { + d.setLUT(lut) + + d.SendCommand(END_OPTION) + d.SendData(lut[153]) + + d.SendCommand(GATE_DRIVING_VOLTAGE) + d.SendData(lut[154]) + + d.SendCommand(SOURCE_DRIVING_VOLTAGE) + d.SendData(lut[155]) + d.SendData(lut[156]) + d.SendData(lut[157]) + + d.SendCommand(WRITE_VCOM_REGISTER) + d.SendData(lut[158]) +} diff --git a/waveshare-epd/epd2in9v2/registers.go b/waveshare-epd/epd2in9v2/registers.go new file mode 100644 index 0000000..cfed5be --- /dev/null +++ b/waveshare-epd/epd2in9v2/registers.go @@ -0,0 +1,50 @@ +package epd2in9v2 + +// Commands from SSD1680 datasheet +const ( + EPD_WIDTH = 128 + EPD_HEIGHT = 296 + + DRIVER_OUTPUT_CONTROL = 0x01 + GATE_DRIVING_VOLTAGE = 0x03 + SOURCE_DRIVING_VOLTAGE = 0x04 + DEEP_SLEEP_MODE = 0x10 + DATA_ENTRY_MODE = 0x11 + SW_RESET = 0x12 + MASTER_ACTIVATION = 0x20 + DISPLAY_UPDATE_CONTROL_1 = 0x21 + DISPLAY_UPDATE_CONTROL_2 = 0x22 + WRITE_RAM_BW = 0x24 + WRITE_RAM_RED = 0x26 + VCOM_SENSE = 0x28 + VCOM_SENSE_DURATION = 0x29 + PROGRAM_VCOM_OTP = 0x2A + WRITE_VCOM_CONTROL = 0x2B + WRITE_VCOM_REGISTER = 0x2C + OTP_READ_DISPLAY_OPTION = 0x2D + USER_ID_READ = 0x2E + PROGRAM_WS_OTP = 0x30 + LOAD_WS_OTP = 0x31 + WRITE_LUT_REGISTER = 0x32 + PROGRAM_OTP_SELECTION = 0x36 + OTP_SELECTION_CONTROL = 0x37 + WRITE_USER_ID = 0x38 + OTP_PROGRAM_MODE = 0x39 + BORDER_WAVEFORM_CONTROL = 0x3C + END_OPTION = 0x3F + SET_RAM_X_ADDRESS = 0x44 + SET_RAM_Y_ADDRESS = 0x45 + SET_RAM_X_COUNTER = 0x4E + SET_RAM_Y_COUNTER = 0x4F + SET_ANALOG_BLOCK_CONTROL = 0x74 + SET_DIGITAL_BLOCK_CONTROL = 0x7E + + NO_ROTATION Rotation = 0 + ROTATION_90 Rotation = 1 + ROTATION_180 Rotation = 2 + ROTATION_270 Rotation = 3 + + SPEED_DEFAULT Speed = 0 + SPEED_FAST Speed = 1 + SPEED_PARTIAL Speed = 2 +) From 21a7d0a96aeb7f55b9c309606992b5350bb7ebd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Henrique=20Guard=C3=A3o=20Gandarez?= Date: Wed, 1 Apr 2026 14:35:53 -0300 Subject: [PATCH 123/137] ws2812: add brightness control --- ws2812/ws2812.go | 38 ++++++++++++++++++++++++++++---------- ws2812/ws2812_rp2_pio.go | 8 +++++--- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/ws2812/ws2812.go b/ws2812/ws2812.go index 583cbbd..6e1ed35 100644 --- a/ws2812/ws2812.go +++ b/ws2812/ws2812.go @@ -18,7 +18,8 @@ var errUnknownClockSpeed = errors.New("ws2812: unknown CPU clock speed") // Device wraps a pin object for an easy driver interface. type Device struct { Pin machine.Pin - writeColorFunc func(Device, []color.RGBA) error + brightness uint8 + writeColorFunc func(Device, []color.RGBA, uint8) error } // deprecated, use NewWS2812 or NewSK6812 depending on which device you want. @@ -40,10 +41,16 @@ func NewWS2812(pin machine.Pin) Device { func NewSK6812(pin machine.Pin) Device { return Device{ Pin: pin, + brightness: 255, writeColorFunc: writeColorsRGBA, } } +// SetBrightness sets the global brightness (0-255). +func (d *Device) SetBrightness(b uint8) { + d.brightness = b +} + // Write the raw bitstring out using the WS2812 protocol. func (d Device) Write(buf []byte) (n int, err error) { for _, c := range buf { @@ -55,24 +62,35 @@ func (d Device) Write(buf []byte) (n int, err error) { // Write the given color slice out using the WS2812 protocol. // Colors are sent out in the usual GRB(A) format. func (d Device) WriteColors(buf []color.RGBA) (err error) { - return d.writeColorFunc(d, buf) + return d.writeColorFunc(d, buf, d.brightness) } -func writeColorsRGB(d Device, buf []color.RGBA) (err error) { +func writeColorsRGB(d Device, buf []color.RGBA, brightness uint8) (err error) { for _, color := range buf { - d.WriteByte(color.G) // green - d.WriteByte(color.R) // red - err = d.WriteByte(color.B) // blue + r, g, b := applyBrightness(color, brightness) + d.WriteByte(g) // green + d.WriteByte(r) // red + err = d.WriteByte(b) // blue } return } -func writeColorsRGBA(d Device, buf []color.RGBA) (err error) { +func writeColorsRGBA(d Device, buf []color.RGBA, brightness uint8) (err error) { for _, color := range buf { - d.WriteByte(color.G) // green - d.WriteByte(color.R) // red - d.WriteByte(color.B) // blue + r, g, b := applyBrightness(color, brightness) + + d.WriteByte(g) // green + d.WriteByte(r) // red + d.WriteByte(b) // blue err = d.WriteByte(color.A) // alpha } return } + +// applyBrightness scales a color by the brightness value. +func applyBrightness(c color.RGBA, brightness uint8) (r, g, b uint8) { + r = uint8((uint16(c.R) * uint16(brightness)) >> 8) + g = uint8((uint16(c.G) * uint16(brightness)) >> 8) + b = uint8((uint16(c.B) * uint16(brightness)) >> 8) + return +} diff --git a/ws2812/ws2812_rp2_pio.go b/ws2812/ws2812_rp2_pio.go index 36259e5..ffd4d0a 100644 --- a/ws2812/ws2812_rp2_pio.go +++ b/ws2812/ws2812_rp2_pio.go @@ -22,10 +22,12 @@ func newWS2812Device(pin machine.Pin) Device { return Device{Pin: pin, writeColorFunc: writeColorsRGB} } return Device{ - Pin: pin, - writeColorFunc: func(_ Device, buf []color.RGBA) error { + Pin: pin, + brightness: 255, + writeColorFunc: func(_ Device, buf []color.RGBA, brightness uint8) error { for _, c := range buf { - ws.PutRGB(c.R, c.G, c.B) + r, g, b := applyBrightness(c, brightness) + ws.PutRGB(r, g, b) } return nil }, From a0c5da601f457b98ae242d2d6beee1cde4125331 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 24 Mar 2026 15:02:56 +0100 Subject: [PATCH 124/137] unoqmatrix: LED matrix on the Arduino Uno Q Signed-off-by: deadprogram --- examples/unoqmatrix/main.go | 54 ++++++ unoqmatrix/matrix.go | 290 ++++++++++++++++++++++++++++++++ unoqmatrix/matrix_test.go | 325 ++++++++++++++++++++++++++++++++++++ unoqmatrix/matrix_tinygo.go | 36 ++++ 4 files changed, 705 insertions(+) create mode 100644 examples/unoqmatrix/main.go create mode 100644 unoqmatrix/matrix.go create mode 100644 unoqmatrix/matrix_test.go create mode 100644 unoqmatrix/matrix_tinygo.go diff --git a/examples/unoqmatrix/main.go b/examples/unoqmatrix/main.go new file mode 100644 index 0000000..89695f8 --- /dev/null +++ b/examples/unoqmatrix/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "machine" + + "image/color" + "math/rand" + + "tinygo.org/x/drivers/unoqmatrix" +) + +var on = color.RGBA{255, 255, 255, 255} + +func main() { + display := unoqmatrix.NewFromBasePin(machine.PF0) + display.ClearDisplay() + + w, h := display.Size() + x := int16(0) + y := int16(0) + deltaX := int16(1) + deltaY := int16(1) + + for { + pixel := display.GetPixel(x, y) + if pixel.R != 0 || pixel.G != 0 || pixel.B != 0 { + display.ClearDisplay() + x = 1 + int16(rand.Int31n(3)) + y = 1 + int16(rand.Int31n(3)) + deltaX = 1 + deltaY = 1 + if rand.Int31n(2) == 0 { + deltaX = -1 + } + if rand.Int31n(2) == 0 { + deltaY = -1 + } + } + display.SetPixel(x, y, on) + + x += deltaX + y += deltaY + + if x == 0 || x == w-1 { + deltaX = -deltaX + } + + if y == 0 || y == h-1 { + deltaY = -deltaY + } + + display.Display() + } +} diff --git a/unoqmatrix/matrix.go b/unoqmatrix/matrix.go new file mode 100644 index 0000000..7430c08 --- /dev/null +++ b/unoqmatrix/matrix.go @@ -0,0 +1,290 @@ +// Package unoqmatrix provides a driver for the UnoQMatrix LED matrix display. +// +// The UnoQMatrix is an 8x13 LED matrix display that can be controlled using a single pin. +// It uses a multiplexing technique to control the LEDs, which allows for a large number of LEDs to be controlled with fewer pins. +// +// This driver provides basic functionality to set individual pixels, clear the display, and refresh the display. +// +// Note: The UnoQMatrix does not support brightness control or color depth. Each pixel can only be turned on or off. +// Could it suppport brightness control by using PWM on the pin? To be investigated. +package unoqmatrix + +import ( + "image/color" + "time" + + pin "tinygo.org/x/drivers/internal/pin" +) + +type Config struct { + // Rotation of the LED matrix. + Rotation uint8 +} + +// Valid values: +// +// 0: regular orientation, (0 degree rotation) +// 1: 90 degree rotation clockwise +// 2: 180 degree rotation clockwise +// 3: 270 degree rotation clockwise +const ( + RotationNormal = 0 + Rotation90 = 1 + Rotation180 = 2 + Rotation270 = 3 +) + +const ( + ledRows = 8 + ledCols = 13 + + pixelRefreshDelay = 10 * time.Microsecond +) + +// CharlieplexPin represents a pin used for charlieplexing. +// It must be able to drive high/low (output mode) and float (high-impedance/input mode). +// +// Example construction from a machine.Pin using the pin HAL pattern: +// +// var isOutput bool +// cp := unoqmatrix.CharlieplexPin{ +// Set: pin.OutputFunc(func(level bool) { +// if !isOutput { +// p.Configure(machine.PinConfig{Mode: machine.PinOutput}) +// isOutput = true +// } +// p.Set(level) +// }), +// Float: func() { +// if isOutput { +// p.Configure(machine.PinConfig{Mode: machine.PinInput}) +// isOutput = false +// } +// }, +// } +type CharlieplexPin struct { + Set pin.OutputFunc // Drive pin high (true) or low (false); auto-configures to output mode. + Float func() // Put pin into high-impedance (input) mode. +} + +const numPins = 11 + +// Device represents the UnoQMatrix LED matrix display. +type Device struct { + pins [numPins]CharlieplexPin + buffer [ledRows][ledCols]color.RGBA + rotation uint8 +} + +// New returns a new unoqmatrix driver. +// The provided pins are the 11 charlieplex pins used to control the LED matrix. +func New(pins [numPins]CharlieplexPin) Device { + return Device{pins: pins} +} + +// Configure sets up the device. +func (d *Device) Configure(cfg Config) { + d.SetRotation(cfg.Rotation) +} + +// SetRotation changes the rotation of the LED matrix. +// +// Valid values for rotation: +// +// 0: regular orientation, (0 degree rotation) +// 1: 90 degree rotation clockwise +// 2: 180 degree rotation clockwise +// 3: 270 degree rotation clockwise +func (d *Device) SetRotation(rotation uint8) { + d.rotation = rotation % 4 +} + +// SetPixel sets the color of a specific pixel. +func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { + d.buffer[y][x] = c +} + +// GetPixel returns the color of a specific pixel. +func (d *Device) GetPixel(x int16, y int16) color.RGBA { + return d.buffer[y][x] +} + +// Display sends the buffer (if any) to the screen. +// Only lights active (non-black) pixels, and resets only the 2 previously +// driven pins between LEDs instead of all 11, making each refresh cycle +// proportional to the number of lit LEDs. +func (d *Device) Display() error { + d.clearDisplay() + + var lastIdx0, lastIdx1 uint8 + hasLast := false + + for row := 0; row < ledRows; row++ { + for col := 0; col < ledCols; col++ { + c := d.buffer[row][col] + if c.R == 0 && c.G == 0 && c.B == 0 { + continue + } + + idx := row*ledCols + col + if idx < 0 || idx >= len(pinMapping) { + continue + } + + // Float only the two pins that were driving the previous LED. + if hasLast { + d.pins[lastIdx0].Float() + d.pins[lastIdx1].Float() + } + hasLast = true + + idx0 := pinMapping[idx][0] + idx1 := pinMapping[idx][1] + d.pins[idx0].Set.High() + d.pins[idx1].Set.Low() + lastIdx0 = idx0 + lastIdx1 = idx1 + + time.Sleep(pixelRefreshDelay) + } + } + + // Float the last driven LED. + if hasLast { + d.pins[lastIdx0].Float() + d.pins[lastIdx1].Float() + } + + return nil +} + +// ClearDisplay turns off all the LEDs on the display. +func (d *Device) ClearDisplay() { + for row := 0; row < ledRows; row++ { + for col := 0; col < ledCols; col++ { + d.buffer[row][col] = color.RGBA{0, 0, 0, 255} + } + } +} + +// Size returns the current size of the display. +func (d *Device) Size() (w, h int16) { + return ledCols, ledRows +} + +// pinMapping defines the mapping of LED indices to pin pairs. Each entry corresponds +// to an LED index (0-104) and contains the two pin numbers that need to be set to turn on that LED. +// based on https://github.com/arduino/ArduinoCore-zephyr/blob/main/loader/matrix.inc#L13 +var pinMapping = [][2]uint8{ + {0, 1}, // 0 + {1, 0}, + {0, 2}, + {2, 0}, + {1, 2}, + {2, 1}, + {0, 3}, + {3, 0}, + {1, 3}, + {3, 1}, + {2, 3}, // 10 + {3, 2}, + {0, 4}, + {4, 0}, + {1, 4}, + {4, 1}, + {2, 4}, + {4, 2}, + {3, 4}, + {4, 3}, + {0, 5}, // 20 + {5, 0}, + {1, 5}, + {5, 1}, + {2, 5}, + {5, 2}, + {3, 5}, + {5, 3}, + {4, 5}, + {5, 4}, + {0, 6}, // 30 + {6, 0}, + {1, 6}, + {6, 1}, + {2, 6}, + {6, 2}, + {3, 6}, + {6, 3}, + {4, 6}, + {6, 4}, + {5, 6}, // 40 + {6, 5}, + {0, 7}, + {7, 0}, + {1, 7}, + {7, 1}, + {2, 7}, + {7, 2}, + {3, 7}, + {7, 3}, + {4, 7}, // 50 + {7, 4}, + {5, 7}, + {7, 5}, + {6, 7}, + {7, 6}, + {0, 8}, + {8, 0}, + {1, 8}, + {8, 1}, + {2, 8}, // 60 + {8, 2}, + {3, 8}, + {8, 3}, + {4, 8}, + {8, 4}, + {5, 8}, + {8, 5}, + {6, 8}, + {8, 6}, + {7, 8}, // 70 + {8, 7}, + {0, 9}, + {9, 0}, + {1, 9}, + {9, 1}, + {2, 9}, + {9, 2}, + {3, 9}, + {9, 3}, + {4, 9}, // 80 + {9, 4}, + {5, 9}, + {9, 5}, + {6, 9}, + {9, 6}, + {7, 9}, + {9, 7}, + {8, 9}, + {9, 8}, + {0, 10}, // 90 + {10, 0}, + {1, 10}, + {10, 1}, + {2, 10}, + {10, 2}, + {3, 10}, + {10, 3}, + {4, 10}, + {10, 4}, + {5, 10}, // 100 + {10, 5}, + {6, 10}, + {10, 6}, +} + +// clearDisplay turns off all the LEDs on the display by floating all pins. +func (d *Device) clearDisplay() { + for i := range d.pins { + d.pins[i].Float() + } +} diff --git a/unoqmatrix/matrix_test.go b/unoqmatrix/matrix_test.go new file mode 100644 index 0000000..4e29958 --- /dev/null +++ b/unoqmatrix/matrix_test.go @@ -0,0 +1,325 @@ +package unoqmatrix + +import ( + "image/color" + "testing" + + pin "tinygo.org/x/drivers/internal/pin" +) + +// pinState tracks the state of a mock charlieplex pin. +type pinState struct { + level bool // true=high, false=low + isOutput bool // true=output mode, false=floating (high-Z) +} + +// mockPins creates 11 mock CharlieplexPins and returns them along with their observable state. +func mockPins() ([numPins]CharlieplexPin, *[numPins]pinState) { + var pins [numPins]CharlieplexPin + var states [numPins]pinState + for i := range pins { + idx := i // capture + pins[i] = CharlieplexPin{ + Set: pin.OutputFunc(func(level bool) { + states[idx].isOutput = true + states[idx].level = level + }), + Float: func() { + states[idx].isOutput = false + states[idx].level = false + }, + } + } + return pins, &states +} + +func newTestDevice() (Device, *[numPins]pinState) { + pins, states := mockPins() + d := New(pins) + return d, states +} + +func TestNew(t *testing.T) { + d, _ := newTestDevice() + w, h := d.Size() + if w != ledCols || h != ledRows { + t.Errorf("Size() = (%d, %d), want (%d, %d)", w, h, ledCols, ledRows) + } +} + +func TestSize(t *testing.T) { + d, _ := newTestDevice() + w, h := d.Size() + if w != 13 { + t.Errorf("width = %d, want 13", w) + } + if h != 8 { + t.Errorf("height = %d, want 8", h) + } +} + +func TestSetGetPixel(t *testing.T) { + d, _ := newTestDevice() + c := color.RGBA{R: 255, G: 128, B: 64, A: 255} + + d.SetPixel(3, 2, c) + got := d.GetPixel(3, 2) + if got != c { + t.Errorf("GetPixel(3,2) = %v, want %v", got, c) + } + + // Unset pixel should be zero-value. + got = d.GetPixel(0, 0) + if got != (color.RGBA{}) { + t.Errorf("GetPixel(0,0) = %v, want zero", got) + } +} + +func TestClearDisplay(t *testing.T) { + d, _ := newTestDevice() + on := color.RGBA{R: 255, G: 255, B: 255, A: 255} + off := color.RGBA{A: 255} + + d.SetPixel(0, 0, on) + d.SetPixel(5, 3, on) + d.ClearDisplay() + + for y := int16(0); y < ledRows; y++ { + for x := int16(0); x < ledCols; x++ { + got := d.GetPixel(x, y) + if got != off { + t.Errorf("after ClearDisplay, GetPixel(%d,%d) = %v, want %v", x, y, got, off) + } + } + } +} + +func TestSetRotation(t *testing.T) { + d, _ := newTestDevice() + + tests := []struct { + input uint8 + want uint8 + }{ + {0, 0}, + {1, 1}, + {2, 2}, + {3, 3}, + {4, 0}, // wraps + {7, 3}, // wraps + } + for _, tt := range tests { + d.SetRotation(tt.input) + if d.rotation != tt.want { + t.Errorf("SetRotation(%d): rotation = %d, want %d", tt.input, d.rotation, tt.want) + } + } +} + +func TestConfigure(t *testing.T) { + d, _ := newTestDevice() + d.Configure(Config{Rotation: 2}) + if d.rotation != 2 { + t.Errorf("Configure(Rotation:2): rotation = %d, want 2", d.rotation) + } +} + +func TestDisplayEmptyBuffer(t *testing.T) { + d, states := newTestDevice() + + err := d.Display() + if err != nil { + t.Fatalf("Display() error: %v", err) + } + + // All pins should be floating after displaying an empty buffer. + for i, s := range states { + if s.isOutput { + t.Errorf("pin %d still in output mode after empty Display()", i) + } + } +} + +func TestDisplaySinglePixel(t *testing.T) { + d, states := newTestDevice() + on := color.RGBA{R: 255, G: 255, B: 255, A: 255} + + // LED index 0 -> pinMapping[0] = {0, 1}: pin 0 high, pin 1 low. + d.SetPixel(0, 0, on) + err := d.Display() + if err != nil { + t.Fatalf("Display() error: %v", err) + } + + // After Display completes, all pins should be floating (last LED turned off). + for i, s := range states { + if s.isOutput { + t.Errorf("pin %d still in output mode after Display()", i) + } + } +} + +func TestDisplayMultiplePixels(t *testing.T) { + d, states := newTestDevice() + on := color.RGBA{R: 255, G: 255, B: 255, A: 255} + + d.SetPixel(0, 0, on) // idx 0 -> pins {0,1} + d.SetPixel(1, 0, on) // idx 1 -> pins {1,0} + d.SetPixel(2, 0, on) // idx 2 -> pins {0,2} + + err := d.Display() + if err != nil { + t.Fatalf("Display() error: %v", err) + } + + // All pins floating after display completes. + for i, s := range states { + if s.isOutput { + t.Errorf("pin %d still in output mode after Display()", i) + } + } +} + +// pinEvent records a single pin action during Display(). +type pinEvent struct { + pinIdx int + action string // "high", "low", or "float" +} + +// traceDevice creates a device that records every pin event for verification. +func traceDevice() (Device, *[]pinEvent) { + var pins [numPins]CharlieplexPin + events := &[]pinEvent{} + for i := range pins { + idx := i + pins[i] = CharlieplexPin{ + Set: pin.OutputFunc(func(level bool) { + action := "low" + if level { + action = "high" + } + *events = append(*events, pinEvent{pinIdx: idx, action: action}) + }), + Float: func() { + *events = append(*events, pinEvent{pinIdx: idx, action: "float"}) + }, + } + } + d := New(pins) + return d, events +} + +func TestDisplayDrivesCorrectPins(t *testing.T) { + d, events := traceDevice() + on := color.RGBA{R: 255, A: 255} + + // Set pixel at (0,0) -> LED index 0 -> pinMapping[0] = {0, 1}. + d.SetPixel(0, 0, on) + d.Display() + + // Expected sequence: + // 1. clearDisplay: float pins 0..10 + // 2. Drive LED 0: pin 0 high, pin 1 low + // 3. Cleanup: float pin 0, float pin 1 + + // Find the high/low events (skip initial floats from clearDisplay). + var driveEvents []pinEvent + for _, e := range *events { + if e.action == "high" || e.action == "low" { + driveEvents = append(driveEvents, e) + } + } + + if len(driveEvents) != 2 { + t.Fatalf("expected 2 drive events, got %d: %v", len(driveEvents), driveEvents) + } + if driveEvents[0].pinIdx != 0 || driveEvents[0].action != "high" { + t.Errorf("first drive event = %v, want pin 0 high", driveEvents[0]) + } + if driveEvents[1].pinIdx != 1 || driveEvents[1].action != "low" { + t.Errorf("second drive event = %v, want pin 1 low", driveEvents[1]) + } +} + +func TestDisplaySkipsBlackPixels(t *testing.T) { + d, events := traceDevice() + on := color.RGBA{R: 255, A: 255} + + // Only set one pixel in the middle of the matrix. + d.SetPixel(4, 1, on) // idx = 1*13+4 = 17 -> pinMapping[17] = {4,2} + d.Display() + + var driveEvents []pinEvent + for _, e := range *events { + if e.action == "high" || e.action == "low" { + driveEvents = append(driveEvents, e) + } + } + + // Should only drive one LED's worth of pin events. + if len(driveEvents) != 2 { + t.Fatalf("expected 2 drive events for 1 lit pixel, got %d", len(driveEvents)) + } + if driveEvents[0].pinIdx != 4 || driveEvents[0].action != "high" { + t.Errorf("expected pin 4 high, got %v", driveEvents[0]) + } + if driveEvents[1].pinIdx != 2 || driveEvents[1].action != "low" { + t.Errorf("expected pin 2 low, got %v", driveEvents[1]) + } +} + +func TestDisplayFloatsBetweenLEDs(t *testing.T) { + d, events := traceDevice() + on := color.RGBA{R: 255, A: 255} + + d.SetPixel(0, 0, on) // idx 0 -> {0,1} + d.SetPixel(1, 0, on) // idx 1 -> {1,0} + d.Display() + + // After the initial clearDisplay floats, the sequence for two LEDs should be: + // drive LED0 (pin0 high, pin1 low) + // float pin0, float pin1 (between LEDs) + // drive LED1 (pin1 high, pin0 low) + // float pin1, float pin0 (cleanup) + + // Skip the initial 11 float events from clearDisplay. + postClear := (*events)[numPins:] + + // Verify pin 0 and 1 are floated between the two LEDs. + foundFloatBetween := false + driveCount := 0 + for _, e := range postClear { + if e.action == "high" || e.action == "low" { + driveCount++ + } + // After the first pair of drive events, we should see floats before the next pair. + if driveCount == 2 && e.action == "float" { + foundFloatBetween = true + break + } + } + if !foundFloatBetween { + t.Error("expected float events between LED drives, found none") + } +} + +func TestPinMappingLength(t *testing.T) { + expected := 104 // 8x13 matrix = 104 LEDs + if len(pinMapping) != expected { + t.Errorf("pinMapping has %d entries, want %d", len(pinMapping), expected) + } +} + +func TestPinMappingIndicesInRange(t *testing.T) { + for i, pair := range pinMapping { + if pair[0] >= numPins { + t.Errorf("pinMapping[%d][0] = %d, exceeds numPins (%d)", i, pair[0], numPins) + } + if pair[1] >= numPins { + t.Errorf("pinMapping[%d][1] = %d, exceeds numPins (%d)", i, pair[1], numPins) + } + if pair[0] == pair[1] { + t.Errorf("pinMapping[%d] has same pin for both: %d", i, pair[0]) + } + } +} diff --git a/unoqmatrix/matrix_tinygo.go b/unoqmatrix/matrix_tinygo.go new file mode 100644 index 0000000..148e6a5 --- /dev/null +++ b/unoqmatrix/matrix_tinygo.go @@ -0,0 +1,36 @@ +//go:build baremetal + +package unoqmatrix + +import ( + "machine" + + pin "tinygo.org/x/drivers/internal/pin" +) + +// NewFromBasePin creates a Device from a base machine.Pin. +// It constructs 11 CharlieplexPin values from consecutive pins starting at basePin. +// Each pin lazily switches between output and input mode as needed. +func NewFromBasePin(basePin machine.Pin) Device { + var pins [numPins]CharlieplexPin + for i := range pins { + p := basePin + machine.Pin(i) + var isOutput bool + pins[i] = CharlieplexPin{ + Set: pin.OutputFunc(func(level bool) { + if !isOutput { + p.Configure(machine.PinConfig{Mode: machine.PinOutput}) + isOutput = true + } + p.Set(level) + }), + Float: func() { + if isOutput { + p.Configure(machine.PinConfig{Mode: machine.PinInput}) + isOutput = false + } + }, + } + } + return New(pins) +} From 0034fc511a333f31cd074e28d4dd87afd26100df Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sat, 11 Apr 2026 10:41:34 -0500 Subject: [PATCH 125/137] fill out more constants for lora device --- lora/config.go | 24 ++++++++++++++---------- lora/radio_event.go | 3 +++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/lora/config.go b/lora/config.go index dd61dc1..21cbe29 100644 --- a/lora/config.go +++ b/lora/config.go @@ -60,16 +60,20 @@ const ( ) const ( - Bandwidth_7_8 = iota // 7.8 kHz - Bandwidth_10_4 // 10.4 kHz - Bandwidth_15_6 // 15.6 kHz - Bandwidth_20_8 // 20.8 kHz - Bandwidth_31_25 // 31.25 kHz - Bandwidth_41_7 // 41.7 kHz - Bandwidth_62_5 // 62.5 kHz - Bandwidth_125_0 // 125.0 kHz - Bandwidth_250_0 // 250.0 kHz - Bandwidth_500_0 // 500.0 kHz + Bandwidth_7_8 = iota // 7.8 kHz + Bandwidth_10_4 // 10.4 kHz + Bandwidth_15_6 // 15.6 kHz + Bandwidth_20_8 // 20.8 kHz + Bandwidth_31_25 // 31.25 kHz + Bandwidth_41_7 // 41.7 kHz + Bandwidth_62_5 // 62.5 kHz + Bandwidth_125_0 // 125.0 kHz + Bandwidth_203_125 // 203.125 kHz + Bandwidth_250_0 // 250.0 kHz + Bandwidth_406_25 // 406.25 kHz + Bandwidth_500_0 // 500.0 kHz + Bandwidth_812_5 // 812.5 kHz + Bandwidth_1625_0 // 1625 kHz ) const ( diff --git a/lora/radio_event.go b/lora/radio_event.go index 7cb6426..ece8270 100644 --- a/lora/radio_event.go +++ b/lora/radio_event.go @@ -6,6 +6,9 @@ const ( RadioEventTimeout RadioEventWatchdog RadioEventCrcError + RadioEventValidHeader + RadioEventCadDone + RadioEventCadDetected RadioEventUnhandled ) From 2673cc1e9a5a0c8fcef18a09805d12d5a238a7e2 Mon Sep 17 00:00:00 2001 From: Daniel Esteban Date: Wed, 15 Apr 2026 22:42:39 +0200 Subject: [PATCH 126/137] fix st7789 scroll on rotated displays --- st7789/st7789.go | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/st7789/st7789.go b/st7789/st7789.go index be28a60..b7e30d1 100644 --- a/st7789/st7789.go +++ b/st7789/st7789.go @@ -593,8 +593,18 @@ func (d *DeviceOf[T]) SetScrollArea(topFixedArea, bottomFixedArea int16) { // The screen doesn't use the full 320 pixel height. // Enlarge the bottom fixed area to fill the 320 pixel height, so that // bottomFixedArea starts from the visible bottom of the screen. - topFixedArea += d.rowOffset - bottomFixedArea += (320 - d.height) - d.rowOffset + // + // VSCRDEF/VSCRSADD always operate on physical frame memory rows (0-319), + // regardless of MADCTL. For rotations with MV set (90°/270°), CASET + // addresses physical rows due to row/column exchange, so the physical row + // offset is d.columnOffset (= rowOffsetCfg). For other rotations, + // d.rowOffset is the physical row offset. + physRowOffset := d.rowOffset + if d.rotation == drivers.Rotation90 || d.rotation == drivers.Rotation270 { + physRowOffset = d.columnOffset + } + topFixedArea += physRowOffset + bottomFixedArea += (320 - d.height) - physRowOffset } if d.rotation == drivers.Rotation180 { // The screen is rotated by 180°, so we have to switch the top and @@ -613,10 +623,20 @@ func (d *DeviceOf[T]) SetScrollArea(topFixedArea, bottomFixedArea int16) { // SetScroll sets the vertical scroll address of the display. func (d *DeviceOf[T]) SetScroll(line int16) { - if d.rotation == drivers.Rotation180 { + switch d.rotation { + case drivers.Rotation90: + // With MV set, hardware scroll operates on physical rows, which map to the + // visual X axis. Add the physical row offset (d.columnOffset = rowOffsetCfg) + // so that line=0 addresses the first visible physical row. + line = line + d.columnOffset + case drivers.Rotation180: // The screen is rotated by 180°, so we have to invert the scroll line // (taking care of the RowOffset). line = (319 - d.rowOffset) - line + case drivers.Rotation270: + // With MV+MY, physical rows map to the visual X axis in reverse direction. + // line=0 addresses the last physical row of the visible area. + line = (d.columnOffset + d.height - 1) - line } d.buf[0] = uint8(line >> 8) d.buf[1] = uint8(line) From e960a6ff57dbf7d579a076b7076a2a49902960ae Mon Sep 17 00:00:00 2001 From: Daniel Esteban Date: Thu, 16 Apr 2026 20:12:27 +0200 Subject: [PATCH 127/137] fix issue #858 --- ws2812/ws2812.go | 7 ++++--- ws2812/ws2812_init_other.go | 2 +- ws2812/ws2812_rp2_pio.go | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ws2812/ws2812.go b/ws2812/ws2812.go index 6e1ed35..2066e03 100644 --- a/ws2812/ws2812.go +++ b/ws2812/ws2812.go @@ -35,9 +35,10 @@ func NewWS2812(pin machine.Pin) Device { return newWS2812Device(pin) } -// New returns a new SK6812(RGBA) driver. -// It does not touch the pin object: you have -// to configure it as an output pin before calling New. +// NewSK6812 returns a new SK6812W/RGBW driver (4 channels, GRBW order, 32-bit protocol), for the 3 channels version use NewWS2812 +// Use this for SK6812W strips that have a dedicated white channel controlled via color.A. +// It does not touch the pin object: you have to configure it as an output pin before +// calling this. func NewSK6812(pin machine.Pin) Device { return Device{ Pin: pin, diff --git a/ws2812/ws2812_init_other.go b/ws2812/ws2812_init_other.go index 8d312de..4ba363a 100644 --- a/ws2812/ws2812_init_other.go +++ b/ws2812/ws2812_init_other.go @@ -6,5 +6,5 @@ import "machine" // newWS2812Device creates a WS2812 device using the bit-bang driver. func newWS2812Device(pin machine.Pin) Device { - return Device{Pin: pin, writeColorFunc: writeColorsRGB} + return Device{Pin: pin, brightness: 255, writeColorFunc: writeColorsRGB} } diff --git a/ws2812/ws2812_rp2_pio.go b/ws2812/ws2812_rp2_pio.go index ffd4d0a..2cc29e1 100644 --- a/ws2812/ws2812_rp2_pio.go +++ b/ws2812/ws2812_rp2_pio.go @@ -15,11 +15,11 @@ import ( func newWS2812Device(pin machine.Pin) Device { sm, err := pio.PIO0.ClaimStateMachine() if err != nil { - return Device{Pin: pin, writeColorFunc: writeColorsRGB} + return Device{Pin: pin, brightness: 255, writeColorFunc: writeColorsRGB} } ws, err := piolib.NewWS2812B(sm, pin) if err != nil { - return Device{Pin: pin, writeColorFunc: writeColorsRGB} + return Device{Pin: pin, brightness: 255, writeColorFunc: writeColorsRGB} } return Device{ Pin: pin, From 1c10dea4438f9cbc54f63851d69d2e5075542ae5 Mon Sep 17 00:00:00 2001 From: nobonobo Date: Mon, 20 Apr 2026 18:51:56 +0900 Subject: [PATCH 128/137] add support extended id for mcp2515 (#857) * add support extended id for mcp2515 * fix mcp2512 example code * revert Pin control --- examples/mcp2515/main.go | 2 +- mcp2515/mcp2515.go | 86 +++++++++++++++++++++------------------- mcp2515/registers.go | 7 ++++ 3 files changed, 53 insertions(+), 42 deletions(-) diff --git a/examples/mcp2515/main.go b/examples/mcp2515/main.go index 6b7a84f..739ecf2 100644 --- a/examples/mcp2515/main.go +++ b/examples/mcp2515/main.go @@ -21,7 +21,7 @@ func main() { SDI: machine.SPI0_SDI_PIN, Mode: 0}) can := mcp2515.New(spi, csPin) - can.Configure() + can.Configure(mcp2515.Configuration{}) err := can.Begin(mcp2515.CAN500kBps, mcp2515.Clock8MHz) if err != nil { failMessage(err.Error()) diff --git a/mcp2515/mcp2515.go b/mcp2515/mcp2515.go index ac08b5a..2b03096 100644 --- a/mcp2515/mcp2515.go +++ b/mcp2515/mcp2515.go @@ -6,6 +6,7 @@ package mcp2515 // import "tinygo.org/x/drivers/mcp2515" import ( + "encoding/binary" "errors" "fmt" "time" @@ -15,15 +16,30 @@ import ( "tinygo.org/x/drivers/internal/pin" ) +var ( + ErrNothingIsReceived = errors.New("readMsg: nothing is received") + ErrRequestNewModeMaxTimeEx = errors.New("requestNewMode max time expired") + ErrLengthIsLongerThanCapacity = errors.New("length is longer than capacity") + ErrTxTimeout = errors.New("Tx: Tx timeout") + ErrInvalidDirection = errors.New("invalid direction") + ErrInvalidParameter = errors.New("invalid parameter") + ErrCannotExpandBuffer = errors.New("cannot expand buffer (to avoid memory allocation)") +) + // Device wraps MCP2515 SPI CAN Module. type Device struct { spi SPI cs pin.OutputFunc msg *CANMsg + extended bool mcpMode byte configurePins func() } +type Configuration struct { + Extended bool +} + // CANMsg stores CAN message fields. type CANMsg struct { ID uint32 @@ -56,10 +72,11 @@ func New(b drivers.SPI, csPin pin.Output) *Device { } // Configure sets up the device for communication. -func (d *Device) Configure() { +func (d *Device) Configure(cfg Configuration) { if d.configurePins == nil { panic(legacy.ErrConfigBeforeInstantiated) } + d.extended = cfg.Extended d.configurePins() } @@ -117,9 +134,13 @@ func (d *Device) Tx(canid uint32, dlc uint8, data []byte) error { timeoutCount++ } if timeoutCount == timeoutvalue { - return fmt.Errorf("Tx: Tx timeout") + return ErrTxTimeout } - err = d.writeCANMsg(bufNum, canid, 0, 0, dlc, data) + ext := byte(0) + if d.extended { + ext = 1 + } + err = d.writeCANMsg(bufNum, canid, ext, 0, dlc, data) if err != nil { return err } @@ -389,7 +410,7 @@ func (d *Device) configRate(speed, clock byte) error { set = false } if !set { - return errors.New("invalid parameter") + return ErrInvalidParameter } if err := d.setRegister(mcpCNF1, cfg1); err != nil { return err @@ -449,7 +470,7 @@ func (d *Device) readMsg() error { return err } } else { - return fmt.Errorf("readMsg: nothing is received") + return ErrNothingIsReceived } return nil @@ -556,39 +577,22 @@ func (d *Device) writeCANMsg(bufNum uint8, canid uint32, ext, rtrBit, dlc uint8, } func (s *SPI) setTxBufData(canid uint32, ext, rtrBit, dlc uint8, data []byte) error { - canid = canid & 0x0FFFF + var id [4]byte if ext == 1 { - // TODO: add Extended ID - err := s.setTxData(0) - if err != nil { - return err - } - err = s.setTxData(0) - if err != nil { - return err - } - err = s.setTxData(0) - if err != nil { - return err - } - err = s.setTxData(0) - if err != nil { - return err - } + canid = canid & extidBottom29Mask + extended_id := canid + high_11 := extended_id & extidTop11WriteMask + low_18 := extended_id & extidBottom18Mask + high_11 <<= 3 + extended_id_shifted := high_11 | low_18 + canid = extended_id_shifted | extidFlagMask } else { - err := s.setTxData(byte(canid >> 3)) - if err != nil { - return err - } - err = s.setTxData(byte((canid & 0x07) << 5)) - if err != nil { - return err - } - err = s.setTxData(0) - if err != nil { - return err - } - err = s.setTxData(0) + canid = canid & stdidBottom11Mask + canid <<= 16 + 5 + } + binary.BigEndian.PutUint32(id[:], canid) + for _, b := range id { + err := s.setTxData(b) if err != nil { return err } @@ -785,7 +789,7 @@ func (d *Device) requestNewMode(newMode byte) error { if r&modeMask == newMode { return nil } else if e := time.Now(); e.Sub(s) > 200*time.Millisecond { - return errors.New("requestNewMode max time expired") + return ErrRequestNewModeMaxTimeEx } } } @@ -861,23 +865,23 @@ func (s *SPI) clearBuffer(dir int) error { return s.setBufferLength(0, dir) } func (s *SPI) setBufferLength(length int, dir int) error { if dir == tx { if length > cap(s.tx) { - return fmt.Errorf("length is longer than capacity") + return ErrLengthIsLongerThanCapacity } s.tx = s.tx[:length] } else if dir == rx { if length > cap(s.rx) { - return fmt.Errorf("length is longer than capacity") + return ErrLengthIsLongerThanCapacity } s.rx = s.rx[:length] } else { - return fmt.Errorf("invalid direction") + return ErrInvalidDirection } return nil } func (s *SPI) setTxData(data byte) error { if len(s.tx) >= bufferSize { - return fmt.Errorf("cannot expand buffer (to avoid memory allocation)") + return ErrCannotExpandBuffer } s.tx = append(s.tx, data) diff --git a/mcp2515/registers.go b/mcp2515/registers.go index 6b7f28a..9ca6661 100644 --- a/mcp2515/registers.go +++ b/mcp2515/registers.go @@ -417,4 +417,11 @@ const ( canFail = 0xff canMaxCharInMessage = 8 + + // for extended id + extidTop11WriteMask = 0x1FFC0000 + extidBottom29Mask = (1 << 29) - 1 // extended id bits + extidBottom18Mask = (1 << 18) - 1 // bottom 18 bits + stdidBottom11Mask = 0x7FF + extidFlagMask = 1 << 19 ) From 9fc1c0aedcd6c4a87ec3b61efb5e34c4c5231eab Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 21 Apr 2026 10:17:44 +0200 Subject: [PATCH 129/137] examples/ws2812: update to accomodate Arduino Uno rename for TinyGo 0.41.0 release Signed-off-by: deadprogram --- examples/ws2812/arduino.go | 2 +- examples/ws2812/others.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ws2812/arduino.go b/examples/ws2812/arduino.go index 6196aec..3712226 100644 --- a/examples/ws2812/arduino.go +++ b/examples/ws2812/arduino.go @@ -1,4 +1,4 @@ -//go:build arduino +//go:build arduino || arduino_uno package main diff --git a/examples/ws2812/others.go b/examples/ws2812/others.go index 27e116e..b23153d 100644 --- a/examples/ws2812/others.go +++ b/examples/ws2812/others.go @@ -1,4 +1,4 @@ -//go:build !digispark && !arduino +//go:build !digispark && !arduino && !arduino_uno package main From fb3062433aae9bc977810ca32e8dba5fc4eba64f Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 21 Apr 2026 10:14:13 +0200 Subject: [PATCH 130/137] license: remove year Signed-off-by: deadprogram --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index c4652c1..398a54a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018-2025 The TinyGo Authors. All rights reserved. +Copyright The TinyGo Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are From 8f372935ac8b1856c545180824030458b216c88f Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 21 Apr 2026 10:14:35 +0200 Subject: [PATCH 131/137] Release 0.35.0 Signed-off-by: deadprogram --- CHANGELOG.md | 37 +++++++++++++++++++++++++++++++++++++ README.md | 2 +- version.go | 2 +- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e93ba19..cce73ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,40 @@ +0.35.0 +--- +- **new devices** + - **unoqmatrix** + - LED matrix on the Arduino Uno Q + - **waveshare-epd (ssd1680)** + - Add driver for Waveshare 2.9 inch v2 e-paper display + +- **enhancements** + - **gps** + - add UBX config command support (#831) + - improve implementation for UBX config commands + - revamp validSentence() to avoid heap allocation for errors + - export some errors for checking/suppression from client + - improvements and corrections for config commands + - **lora** + - fill out more constants for lora device + - **mcp2515** + - add support for extended CAN IDs (#857) + - **si5351** + - complete refactor for more complete interface + - **st7735** + - remove dependency on the machine package + - **sx127x** + - add functions used for FSK radio communication + - **ws2812** + - add brightness control + - add PIO support for RP2040/RP2350 + +- **bugfixes** + - **st7789** + - fix scroll on rotated displays + - fix driver when rotated 90º + - **ws2812** + - fix brightness control issues (#858) + + 0.34.0 --- - **core** diff --git a/README.md b/README.md index d049163..fbcef08 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PkgGoDev](https://pkg.go.dev/badge/tinygo.org/x/drivers)](https://pkg.go.dev/tinygo.org/x/drivers) [![Build](https://github.com/tinygo-org/drivers/actions/workflows/build.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/drivers/actions/workflows/build.yml) -This package provides a collection of 139 different hardware drivers for devices such as sensors, displays, wireless adaptors, and actuators, that can be used together with [TinyGo](https://tinygo.org). +This package provides a collection of over 140 different hardware drivers for devices such as sensors, displays, wireless adaptors, and actuators, that can be used together with [TinyGo](https://tinygo.org). For the complete list, please see: https://tinygo.org/docs/reference/devices/ diff --git a/version.go b/version.go index d8ed5c8..0b44de9 100644 --- a/version.go +++ b/version.go @@ -2,4 +2,4 @@ package drivers // Version returns a user-readable string showing the version of the drivers package for support purposes. // Update this value before release of new version of software. -const Version = "0.34.0" +const Version = "0.35.0" From 62663c1832cd80b2480ada89bffe08f3816c7e0b Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Mon, 4 May 2026 07:42:03 -0500 Subject: [PATCH 132/137] add SPI driver for semtech sx128x chips (#864) * add SPI driver for semtech sx128x chips Co-authored-by: Copilot * handle busy loop better * switch to time based busy timeout Co-authored-by: Copilot * comment functions * start on using types for function inputs * use types where applicable and align with datasheet more Co-authored-by: Copilot * work on exporting less constants * only export "actionable" errors * combine identical constants * add crude lora rx and tx examples * change from type aliases to local types --------- Co-authored-by: Copilot --- examples/sx128x/lora_rx/main.go | 106 +++++ examples/sx128x/lora_tx/main.go | 96 ++++ sx128x/README.md | 7 + sx128x/commands.go | 52 +++ sx128x/constants.go | 356 +++++++++++++++ sx128x/errors.go | 19 + sx128x/registers.go | 69 +++ sx128x/sx128x.go | 768 ++++++++++++++++++++++++++++++++ 8 files changed, 1473 insertions(+) create mode 100644 examples/sx128x/lora_rx/main.go create mode 100644 examples/sx128x/lora_tx/main.go create mode 100644 sx128x/README.md create mode 100644 sx128x/commands.go create mode 100644 sx128x/constants.go create mode 100644 sx128x/errors.go create mode 100644 sx128x/registers.go create mode 100644 sx128x/sx128x.go diff --git a/examples/sx128x/lora_rx/main.go b/examples/sx128x/lora_rx/main.go new file mode 100644 index 0000000..33ea60d --- /dev/null +++ b/examples/sx128x/lora_rx/main.go @@ -0,0 +1,106 @@ +package main + +import ( + "errors" + "machine" + "runtime" + "time" + + "tinygo.org/x/drivers/sx128x" +) + +var ( + // pin mapping specific to the lilygo t3s3, change as needed for your board + sdoPin = machine.GPIO6 + sdiPin = machine.GPIO3 + sckPin = machine.GPIO5 + nssPin = machine.GPIO7 + busyPin = machine.GPIO36 + resetPin = machine.GPIO8 + dio1Pin = machine.GPIO9 +) + +func setupPins() { + nssPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + nssPin.Set(true) + + resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + resetPin.Set(true) + + busyPin.Configure(machine.PinConfig{Mode: machine.PinInput}) + dio1Pin.Configure(machine.PinConfig{Mode: machine.PinInput}) +} + +func main() { + setupPins() + + spi := machine.SPI0 + spi.Configure(machine.SPIConfig{ + Mode: 0, + Frequency: 8 * 1e6, + SDO: sdoPin, + SDI: sdiPin, + SCK: sckPin, + }) + + radio := sx128x.New( + spi, + nssPin, + resetPin, + busyPin, + ) + + radio.WaitWhileBusy(time.Second) + SetupLora(radio) + + for { + data, err := Rx(radio) + if err != nil { + println("failed to receive:", err) + } else { + println("received:", string(data)) + } + } +} + +func SetupLora(radio *sx128x.Device) { + radio.SetStandby(sx128x.STANDBY_RC) + radio.SetPacketType(sx128x.PACKET_TYPE_LORA) + radio.SetRegulatorMode(sx128x.REGULATOR_DC_DC) + + radio.SetRfFrequency(2400000000) // 2.4Ghz + radio.SetModulationParamsLoRa(sx128x.LORA_SF_9, sx128x.LORA_BW_1600, sx128x.LORA_CR_4_7) + + // section 14.4.1 shows required register setting for setting up LoRa operations. These depend on the chosen spreading factor. + radio.WriteRegister(0x925, []byte{0x32}) + radio.WriteRegister(0x93C, []byte{0x01}) + + radio.SetTxParams(13, sx128x.RADIO_RAMP_02_US) + radio.SetPacketParamsLoRa(12, sx128x.LORA_HEADER_EXPLICIT, 0xFF, sx128x.LORA_CRC_DISABLE, sx128x.LORA_IQ_STD) + radio.WriteRegister(sx128x.REG_LORA_SYNC_WORD_MSB, []byte{0x14, 0x24}) // full sync word is 0x1424 + +} + +func Rx(radio *sx128x.Device) ([]byte, error) { + radio.SetStandby(sx128x.STANDBY_RC) + radio.SetDioIrqParams(sx128x.IRQ_RX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, sx128x.IRQ_RX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, 0x00, 0x00) + radio.SetBufferBaseAddress(0, 0) + radio.ClearIrqStatus(sx128x.IRQ_ALL_MASK) + radio.SetRx(sx128x.PERIOD_BASE_4_MS, 250) // 4ms * 250 = 1s + // busy wait for IRQ indication + for dio1Pin.Get() == false { + runtime.Gosched() + } + irqStatus, _ := radio.GetIrqStatus() + if irqStatus&sx128x.IRQ_RX_DONE_MASK != 0 { + payloadLength, bufferOffset, err := radio.GetRxBufferStatus() + if err != nil { + return nil, err + } + data, err := radio.ReadBuffer(bufferOffset, payloadLength) + return data, nil + } else if irqStatus&sx128x.IRQ_RX_TX_TIMEOUT_MASK != 0 { + return nil, errors.New("rx timeout") + } + return nil, errors.New("unexpected IRQ status") +} diff --git a/examples/sx128x/lora_tx/main.go b/examples/sx128x/lora_tx/main.go new file mode 100644 index 0000000..248d3a7 --- /dev/null +++ b/examples/sx128x/lora_tx/main.go @@ -0,0 +1,96 @@ +package main + +import ( + "errors" + "machine" + "runtime" + "time" + + "tinygo.org/x/drivers/sx128x" +) + +var ( + // pin mapping specific to the lilygo t3s3, change as needed for your board + sdoPin = machine.GPIO6 + sdiPin = machine.GPIO3 + sckPin = machine.GPIO5 + nssPin = machine.GPIO7 + busyPin = machine.GPIO36 + resetPin = machine.GPIO8 + dio1Pin = machine.GPIO9 +) + +func setupPins() { + nssPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + nssPin.Set(true) + + resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + resetPin.Set(true) + + busyPin.Configure(machine.PinConfig{Mode: machine.PinInput}) + dio1Pin.Configure(machine.PinConfig{Mode: machine.PinInput}) +} + +func main() { + setupPins() + + spi := machine.SPI0 + spi.Configure(machine.SPIConfig{ + Mode: 0, + Frequency: 8 * 1e6, + SDO: sdoPin, + SDI: sdiPin, + SCK: sckPin, + }) + + radio := sx128x.New( + spi, + nssPin, + resetPin, + busyPin, + ) + + radio.WaitWhileBusy(time.Second) + SetupLora(radio) + + for { + Tx(radio, []byte("Hello, world!")) + time.Sleep(1 * time.Second) + } +} + +func SetupLora(radio *sx128x.Device) { + radio.SetStandby(sx128x.STANDBY_RC) + radio.SetPacketType(sx128x.PACKET_TYPE_LORA) + radio.SetRegulatorMode(sx128x.REGULATOR_DC_DC) + + radio.SetRfFrequency(2400000000) // 2.4Ghz + radio.SetModulationParamsLoRa(sx128x.LORA_SF_9, sx128x.LORA_BW_1600, sx128x.LORA_CR_4_7) + + // section 14.4.1 shows required register setting for setting up LoRa operations. These depend on the chosen spreading factor. + radio.WriteRegister(0x925, []byte{0x32}) + radio.WriteRegister(0x93C, []byte{0x01}) + + radio.SetTxParams(13, sx128x.RADIO_RAMP_02_US) + radio.SetPacketParamsLoRa(12, sx128x.LORA_HEADER_EXPLICIT, 0xFF, sx128x.LORA_CRC_DISABLE, sx128x.LORA_IQ_STD) + radio.WriteRegister(sx128x.REG_LORA_SYNC_WORD_MSB, []byte{0x14, 0x24}) // full sync word is 0x1424 + +} + +func Tx(radio *sx128x.Device, data []byte) error { + if len(data) > 255 { + return errors.New("data length exceeds maximum of 255 bytes") + } + radio.SetStandby(sx128x.STANDBY_RC) + radio.SetPacketParamsLoRa(12, sx128x.LORA_HEADER_EXPLICIT, uint8(len(data)&0xFF), sx128x.LORA_CRC_DISABLE, sx128x.LORA_IQ_STD) + radio.SetBufferBaseAddress(0, 0) + radio.WriteBuffer(0, data) + radio.SetDioIrqParams(sx128x.IRQ_TX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, sx128x.IRQ_TX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, 0x00, 0x00) + radio.ClearIrqStatus(sx128x.IRQ_ALL_MASK) + radio.SetTx(sx128x.PERIOD_BASE_4_MS, 250) // 4ms * 250 = 1s + // busy wait for IRQ indication + for dio1Pin.Get() == false { + runtime.Gosched() + } + return nil +} diff --git a/sx128x/README.md b/sx128x/README.md new file mode 100644 index 0000000..1dd0198 --- /dev/null +++ b/sx128x/README.md @@ -0,0 +1,7 @@ +# SX128x Radio +Radio from Semtech in the 2.4 GHz band. This driver uses SPI to communicate with the radio instead of the alternative UART interface. + +## Supported Chips +- [SX1280](https://www.semtech.com/products/wireless-rf/lora-connect/sx1280) +- [SX1281](https://www.semtech.com/products/wireless-rf/lora-connect/sx1281) + diff --git a/sx128x/commands.go b/sx128x/commands.go new file mode 100644 index 0000000..fc294cd --- /dev/null +++ b/sx128x/commands.go @@ -0,0 +1,52 @@ +package sx128x + +const ( + // SX128X SPI commands + cmdGetStatus = uint8(0xC0) + + // Register Access Operations + cmdWriteRegister = uint8(0x18) + cmdReadRegister = uint8(0x19) + + // Data Buffer Operations + cmdWriteBuffer = uint8(0x1A) + cmdReadBuffer = uint8(0x1B) + + // Radio Operation Modes + cmdSetSleep = uint8(0x84) + cmdSetStandby = uint8(0x80) + cmdSetFS = uint8(0xC1) + cmdSetTx = uint8(0x83) + cmdSetRx = uint8(0x82) + cmdSetRxDutyCycle = uint8(0x94) + cmdSetLongPreamble = uint8(0x9B) + cmdSetCAD = uint8(0xC5) + cmdSetTxContinuousWave = uint8(0xD1) + cmdSetContinuousPreamble = uint8(0xD2) + cmdSetAutoTx = uint8(0x98) + cmdSetAutoFS = uint8(0x9E) + + // Radio Configuration + cmdSetPacketType = uint8(0x8A) + cmdGetPacketType = uint8(0x03) + cmdSetRFFrequency = uint8(0x86) + cmdSetTxParams = uint8(0x8E) + cmdSetCADParams = uint8(0x88) + cmdSetBufferBaseAddress = uint8(0x8F) + cmdSetModulationParams = uint8(0x8B) + cmdSetPacketParams = uint8(0x8C) + + // Communication Status Information + cmdGetRxBufferStatus = uint8(0x17) + cmdGetPacketStatus = uint8(0x1D) + cmdGetRSSIInst = uint8(0x1F) + + // IRQ Handling + cmdSetDIOIRQParams = uint8(0x8D) + cmdGetIRQStatus = uint8(0x15) + cmdClearIRQStatus = uint8(0x97) + + // Miscellaneous + cmdSetRegulatorMode = uint8(0x96) + cmdSetSaveContext = uint8(0xD5) +) diff --git a/sx128x/constants.go b/sx128x/constants.go new file mode 100644 index 0000000..aa0e17d --- /dev/null +++ b/sx128x/constants.go @@ -0,0 +1,356 @@ +package sx128x + +type SleepConfig uint8 +type StandbyConfig uint8 +type PeriodBase uint8 +type PacketType uint8 +type RadioRampTime uint8 +type CadSymbolNum uint8 + +// GFSK Modulation Params +type GFSKBLEBitrateBandwidth uint8 +type ModulationIndex uint8 +type ModulationShaping uint8 + +// GFSK Packet Params +type GFSKPreambleLength uint8 +type GFSKSyncWordLength uint8 +type GFSKSyncWordMatch uint8 +type GFSKHeaderType uint8 +type GFSKCrcType uint8 + +// BLE Packet Params +type BLEConnectionState uint8 +type BLECrcType uint8 +type BLETestPayload uint8 + +// FLRC Modulation Params +type FLRCBitrateBandwidth uint8 +type FLRCCodingRate uint8 + +// FLRC Packet Params +type FLRCPreambleLength uint8 +type FLRCSyncWordLength uint8 +type FLRCSyncWordMatch uint8 +type FLRCHeaderType uint8 +type FLRCCrcType uint8 + +// LoRa Modulation Params +type LoRaSpreadingFactor uint8 +type LoRaBandwidth uint8 +type LoRaCodingRate uint8 + +// LoRa Packet Params +type LoRaHeaderType uint8 +type LoRaCrcType uint8 +type LoRaIqType uint8 + +// Misc +type RegulatorMode uint8 +type IRQMask = uint16 +type CircuitMode uint8 +type CommandStatus uint8 + +// Packet Status +type GFSKPacketInfo uint8 +type BLEPacketInfo uint8 +type FLRCPacketInfo uint8 + +const ( + whiteningDisable = 0x00 + whiteningEnable = 0x08 + + // Circuit Mode + circuitModeMask = uint8(0b11100000) + CIRCUIT_MODE_STDBY_RC = CircuitMode(0x2) + CIRCUIT_MODE_STDBY_XOSC = CircuitMode(0x3) + CIRCUIT_MODE_FS = CircuitMode(0x4) + CIRCUIT_MODE_RX = CircuitMode(0x5) + CIRCUIT_MODE_TX = CircuitMode(0x6) + + // Command Status + commandStatusMask = uint8(0b00011100) + COMMAND_STATUS_SUCCESS = CommandStatus(0x1) + COMMAND_STATUS_DATA_AVAILABLE = CommandStatus(0x2) + COMMAND_STATUS_TIMEOUT = CommandStatus(0x3) + COMMAND_STATUS_PROCESSING_ERROR = CommandStatus(0x4) + COMMAND_STATUS_EXECUTION_ERROR = CommandStatus(0x5) + COMMAND_STATUS_TX_DONE = CommandStatus(0x6) + + // SleepConfig + SLEEP_DATA_BUFFER_RETAIN = SleepConfig(2) + SLEEP_DATA_RAM_RETAIN = SleepConfig(1) + + // StandbyConfig + STANDBY_RC = StandbyConfig(0) + STANDBY_XOSC = StandbyConfig(1) + + // PeriodBase + PERIOD_BASE_15_625_US = PeriodBase(0) + PERIOD_BASE_62_5_US = PeriodBase(1) + PERIOD_BASE_1_MS = PeriodBase(2) + PERIOD_BASE_4_MS = PeriodBase(3) + + // PacketType + PACKET_TYPE_GFSK = PacketType(0x00) // default + PACKET_TYPE_LORA = PacketType(0x01) + PACKET_TYPE_RANGING = PacketType(0x02) + PACKET_TYPE_FLRC = PacketType(0x03) + PACKET_TYPE_BLE = PacketType(0x04) + + // RampTime + RADIO_RAMP_02_US = RadioRampTime(0x00) + RADIO_RAMP_04_US = RadioRampTime(0x20) + RADIO_RAMP_06_US = RadioRampTime(0x40) + RADIO_RAMP_08_US = RadioRampTime(0x60) + RADIO_RAMP_10_US = RadioRampTime(0x80) + RADIO_RAMP_12_US = RadioRampTime(0xA0) + RADIO_RAMP_16_US = RadioRampTime(0xC0) + RADIO_RAMP_20_US = RadioRampTime(0xE0) + + // CadSymbolNum + LORA_CAD_01_SYMBOL = CadSymbolNum(0x00) + LORA_CAD_02_SYMBOLS = CadSymbolNum(0x20) + LORA_CAD_04_SYMBOLS = CadSymbolNum(0x40) + LORA_CAD_08_SYMBOLS = CadSymbolNum(0x60) + LORA_CAD_16_SYMBOLS = CadSymbolNum(0x80) + + // GFSK Modulation Params + // Bitrate + Bandwidth - same for BLE + GFSK_BLE_BR_2_000_BW_2_4 = GFSKBLEBitrateBandwidth(0x04) + GFSK_BLE_BR_1_600_BW_2_4 = GFSKBLEBitrateBandwidth(0x28) + GFSK_BLE_BR_1_000_BW_2_4 = GFSKBLEBitrateBandwidth(0x4C) + GFSK_BLE_BR_1_000_BW_1_2 = GFSKBLEBitrateBandwidth(0x45) + GFSK_BLE_BR_0_800_BW_2_4 = GFSKBLEBitrateBandwidth(0x70) + GFSK_BLE_BR_0_800_BW_1_2 = GFSKBLEBitrateBandwidth(0x69) + GFSK_BLE_BR_0_500_BW_1_2 = GFSKBLEBitrateBandwidth(0x8D) + GFSK_BLE_BR_0_500_BW_0_6 = GFSKBLEBitrateBandwidth(0x86) + GFSK_BLE_BR_0_400_BW_1_2 = GFSKBLEBitrateBandwidth(0xB1) + GFSK_BLE_BR_0_400_BW_0_6 = GFSKBLEBitrateBandwidth(0xAA) + GFSK_BLE_BR_0_250_BW_0_6 = GFSKBLEBitrateBandwidth(0xCE) + GFSK_BLE_BR_0_250_BW_0_3 = GFSKBLEBitrateBandwidth(0xC7) + GFSK_BLE_BR_0_125_BW_0_3 = GFSKBLEBitrateBandwidth(0xEF) + + // Modulation Index - same for BLE + MOD_IND_0_35 = ModulationIndex(0x00) + MOD_IND_0_5 = ModulationIndex(0x01) + MOD_IND_0_75 = ModulationIndex(0x02) + MOD_IND_1_00 = ModulationIndex(0x03) + MOD_IND_1_25 = ModulationIndex(0x04) + MOD_IND_1_50 = ModulationIndex(0x05) + MOD_IND_1_75 = ModulationIndex(0x06) + MOD_IND_2_00 = ModulationIndex(0x07) + MOD_IND_2_25 = ModulationIndex(0x08) + MOD_IND_2_50 = ModulationIndex(0x09) + MOD_IND_2_75 = ModulationIndex(0x0A) + MOD_IND_3_00 = ModulationIndex(0x0B) + MOD_IND_3_25 = ModulationIndex(0x0C) + MOD_IND_3_50 = ModulationIndex(0x0D) + MOD_IND_3_75 = ModulationIndex(0x0E) + MOD_IND_4_00 = ModulationIndex(0x0F) + + // Modulation Shaping - same for BLE and FLRC + MOD_SHAPING_OFF = ModulationShaping(0x00) + MOD_SHAPING_1_0 = ModulationShaping(0x10) + MOD_SHAPING_0_5 = ModulationShaping(0x20) + + // GFSK Packet Params + // Preamble Length + GFSK_PREAMBLE_LENGTH_04_BITS = GFSKPreambleLength(0x00) + GFSK_PREAMBLE_LENGTH_08_BITS = GFSKPreambleLength(0x10) + GFSK_PREAMBLE_LENGTH_12_BITS = GFSKPreambleLength(0x20) + GFSK_PREAMBLE_LENGTH_16_BITS = GFSKPreambleLength(0x30) + GFSK_PREAMBLE_LENGTH_20_BITS = GFSKPreambleLength(0x40) + GFSK_PREAMBLE_LENGTH_24_BITS = GFSKPreambleLength(0x50) + GFSK_PREAMBLE_LENGTH_28_BITS = GFSKPreambleLength(0x60) + GFSK_PREAMBLE_LENGTH_32_BITS = GFSKPreambleLength(0x70) + + // Sync Word Length + GFSK_SYNC_WORD_LEN_1_B = GFSKSyncWordLength(0x00) + GFSK_SYNC_WORD_LEN_2_B = GFSKSyncWordLength(0x02) + GFSK_SYNC_WORD_LEN_3_B = GFSKSyncWordLength(0x04) + GFSK_SYNC_WORD_LEN_4_B = GFSKSyncWordLength(0x06) + GFSK_SYNC_WORD_LEN_5_B = GFSKSyncWordLength(0x08) + + // Sync Word Match + GFSK_SYNCWORD_MATCH_OFF = GFSKSyncWordMatch(0x00) + GFSK_SYNCWORD_MATCH_1 = GFSKSyncWordMatch(0x10) + GFSK_SYNCWORD_MATCH_2 = GFSKSyncWordMatch(0x20) + GFSK_SYNCWORD_MATCH_1_2 = GFSKSyncWordMatch(0x30) + GFSK_SYNCWORD_MATCH_3 = GFSKSyncWordMatch(0x40) + GFSK_SYNCWORD_MATCH_1_3 = GFSKSyncWordMatch(0x50) + GFSK_SYNCWORD_MATCH_2_3 = GFSKSyncWordMatch(0x60) + GFSK_SYNCWORD_MATCH_1_2_3 = GFSKSyncWordMatch(0x70) + + // GFSK Header Type + GFSK_HEADER_FIXED_LENGTH = GFSKHeaderType(0x00) + GFSK_HEADER_VARIABLE_LENGTH = GFSKHeaderType(0x20) + + // GFSK CRC Type + GFSK_CRC_OFF = GFSKCrcType(0x00) + GFSK_CRC_1_BYTE = GFSKCrcType(0x10) + GFSK_CRC_2_BYTES = GFSKCrcType(0x20) + + // BLE Packet Params + // Connection State + BLE_MASTER_SLAVE = BLEConnectionState(0x00) + BLE_ADVERTISER = BLEConnectionState(0x02) + BLE_TX_TEST_MODE = BLEConnectionState(0x04) + BLE_RX_TEST_MODE = BLEConnectionState(0x06) + BLE_RXTX_TEST_MODE = BLEConnectionState(0x08) + + // CRC Type + BLE_CRC_OFF = BLECrcType(0x00) + BLE_CRC_3_BYTES = BLECrcType(0x10) + + // BLE Test Payload + BLE_PAYLOAD_PRBS_9 = BLETestPayload(0x00) + BLE_PAYLOAD_EYELONG_1_0 = BLETestPayload(0x04) + BLE_PAYLOAD_EYESHORT_1_0 = BLETestPayload(0x08) + BLE_PAYLOAD_PRBS_15 = BLETestPayload(0x0C) + BLE_PAYLOAD_ALL_1 = BLETestPayload(0x10) + BLE_PAYLOAD_ALL_0 = BLETestPayload(0x14) + BLE_PAYLOAD_EYELONG_0_1 = BLETestPayload(0x18) + BLE_PAYLOAD_EYESHORT_0_1 = BLETestPayload(0x1C) + + // FLRC Modulation Params + // Bitrate + Bandwidth + FLRC_BR_1_300_BW_1_2 = FLRCBitrateBandwidth(0x45) + FLRC_BR_1_000_BW_1_2 = FLRCBitrateBandwidth(0x69) + FLRC_BR_0_650_BW_0_6 = FLRCBitrateBandwidth(0x86) + FLRC_BR_0_520_BW_0_6 = FLRCBitrateBandwidth(0xAA) + FLRC_BR_0_325_BW_0_3 = FLRCBitrateBandwidth(0xC7) + FLRC_BR_0_260_BW_0_3 = FLRCBitrateBandwidth(0xEB) + + // Coding Rate + FLRC_CR_1_2 = FLRCCodingRate(0x00) // 1/2 + FLRC_CR_3_4 = FLRCCodingRate(0x02) // 3/4 + FLRC_CR_1_0 = FLRCCodingRate(0x04) // 1 + + // FLRC Packet Params + // Preamble Length + FLRC_PREAMBLE_LENGTH_4_BITS = FLRCPreambleLength(0x00) + FLRC_PREAMBLE_LENGTH_8_BITS = FLRCPreambleLength(0x10) + FLRC_PREAMBLE_LENGTH_12_BITS = FLRCPreambleLength(0x20) + FLRC_PREAMBLE_LENGTH_16_BITS = FLRCPreambleLength(0x30) + FLRC_PREAMBLE_LENGTH_20_BITS = FLRCPreambleLength(0x40) + FLRC_PREAMBLE_LENGTH_24_BITS = FLRCPreambleLength(0x50) + FLRC_PREAMBLE_LENGTH_28_BITS = FLRCPreambleLength(0x60) + FLRC_PREAMBLE_LENGTH_32_BITS = FLRCPreambleLength(0x70) + + // Sync Word Length + FLRC_SYNC_WORD_LEN_0 = FLRCSyncWordLength(0x00) + FLRC_SYNC_WORD_LEN_32_BITS = FLRCSyncWordLength(0x04) + + // Sync Word Match + FLRC_SYNC_WORD_MATCH_DISABLE = FLRCSyncWordMatch(0x00) // Disable Sync Word + FLRC_SYNC_WORD_MATCH_1 = FLRCSyncWordMatch(0x10) // Sync Word 1 + FLRC_SYNC_WORD_MATCH_2 = FLRCSyncWordMatch(0x20) // Sync Word 2 + FLRC_SYNC_WORD_MATCH_1_2 = FLRCSyncWordMatch(0x30) // Sync Word 1 or Sync Word 2 + FLRC_SYNC_WORD_MATCH_3 = FLRCSyncWordMatch(0x40) // Sync Word 3 + FLRC_SYNC_WORD_MATCH_1_3 = FLRCSyncWordMatch(0x50) // Sync Word 1 or Sync Word 3 + FLRC_SYNC_WORD_MATCH_2_3 = FLRCSyncWordMatch(0x60) // Sync Word 2 or Sync Word 3 + FLRC_SYNC_WORD_MATCH_1_2_3 = FLRCSyncWordMatch(0x70) // Sync Word 1 or Sync Word 2 or Sync Word 3 + + // Header Type + FLRC_HEADER_FIXED_LENGTH = FLRCHeaderType(0x00) + FLRC_HEADER_VARIABLE_LENGTH = FLRCHeaderType(0x20) + + // CRC Type + FLRC_CRC_OFF = FLRCCrcType(0x00) + FLRC_CRC_1_BYTE = FLRCCrcType(0x10) + FLRC_CRC_2_BYTES = FLRCCrcType(0x20) + FLRC_CRC_3_BYTES = FLRCCrcType(0x30) + + // LoRa Modulation Params + + // SpreadingFactor + LORA_SF_5 = LoRaSpreadingFactor(0x50) + LORA_SF_6 = LoRaSpreadingFactor(0x60) + LORA_SF_7 = LoRaSpreadingFactor(0x70) + LORA_SF_8 = LoRaSpreadingFactor(0x80) + LORA_SF_9 = LoRaSpreadingFactor(0x90) + LORA_SF_10 = LoRaSpreadingFactor(0xA0) + LORA_SF_11 = LoRaSpreadingFactor(0xB0) + LORA_SF_12 = LoRaSpreadingFactor(0xC0) + + // Bandwidth + LORA_BW_1600 = LoRaBandwidth(0x0A) + LORA_BW_800 = LoRaBandwidth(0x18) + LORA_BW_400 = LoRaBandwidth(0x26) + LORA_BW_200 = LoRaBandwidth(0x34) + + // CodingRate + LORA_CR_4_5 = LoRaCodingRate(0x01) + LORA_CR_4_6 = LoRaCodingRate(0x02) + LORA_CR_4_7 = LoRaCodingRate(0x03) + LORA_CR_4_8 = LoRaCodingRate(0x04) + LORA_CR_LI_4_5 = LoRaCodingRate(0x05) + LORA_CR_LI_4_6 = LoRaCodingRate(0x06) + LORA_CR_LI_4_8 = LoRaCodingRate(0x07) + + // LoraPacketParams + // HeaderType + LORA_HEADER_EXPLICIT = LoRaHeaderType(0x00) + LORA_HEADER_IMPLICIT = LoRaHeaderType(0x80) + + // CRC Type + LORA_CRC_ENABLE = LoRaCrcType(0x20) + LORA_CRC_DISABLE = LoRaCrcType(0x00) + + // IQ Type + LORA_IQ_INVERTED = LoRaIqType(0x00) + LORA_IQ_STD = LoRaIqType(0x40) + + // RegulatorMode + REGULATOR_LDO = RegulatorMode(0) + REGULATOR_DC_DC = RegulatorMode(1) + + // IRQ masks + IRQ_ALL_MASK = IRQMask(0xFFFF) + IRQ_NONE_MASK = IRQMask(0x0000) + IRQ_TX_DONE_MASK = IRQMask(0b0000000000000001) + IRQ_RX_DONE_MASK = IRQMask(0b0000000000000010) + IRQ_SYNC_WORD_VALID_MASK = IRQMask(0b0000000000000100) + IRQ_SYNC_WORD_ERROR_MASK = IRQMask(0b0000000000001000) + IRQ_HEADER_VALID_MASK = IRQMask(0b0000000000010000) + IRQ_HEADER_ERROR_MASK = IRQMask(0b0000000000100000) + IRQ_CRC_ERROR_MASK = IRQMask(0b0000000001000000) + IRQ_RANGING_SLAVE_RESPONSE_DONE_MASK = IRQMask(0b0000000010000000) + IRQ_RANGING_SLAVE_RESPONSE_DISCARD_MASK = IRQMask(0b0000000100000000) + IRQ_RANGING_MASTER_RESULT_VALID_MASK = IRQMask(0b0000001000000000) + IRQ_RANGING_MASTER_TIMEOUT_MASK = IRQMask(0b0000010000000000) + IRQ_RANGING_SLAVE_REQUEST_VALID_MASK = IRQMask(0b0000100000000000) + IRQ_CAD_DONE_MASK = IRQMask(0b0001000000000000) + IRQ_CAD_DETECTED_MASK = IRQMask(0b0010000000000000) + IRQ_RX_TX_TIMEOUT_MASK = IRQMask(0b0100000000000000) + IRQ_PREAMBLE_DETECTED_MASK = IRQMask(0b1000000000000000) + IRQ_ADVANCED_RANGING_DONE_MASK = IRQMask(0b1000000000000000) + + // GFSK Packet Info + GFSK_SYNC_ERROR = GFSKPacketInfo(0b1000000) + GFSK_LENGTH_ERROR = GFSKPacketInfo(0b0100000) + GFSK_CRC_ERROR = GFSKPacketInfo(0b0010000) + GFSK_ABORT_ERROR = GFSKPacketInfo(0b0001000) + GFSK_HEADER_RECEIVED = GFSKPacketInfo(0b0000100) + GFSK_PACKET_RECEIVED = GFSKPacketInfo(0b0000010) + GFSK_PACKET_CRTL_BUSY = GFSKPacketInfo(0b0000001) + + // BLE Packet Info + BLE_SYNC_ERROR = BLEPacketInfo(0b1000000) + BLE_LENGTH_ERROR = BLEPacketInfo(0b0100000) + BLE_CRC_ERROR = BLEPacketInfo(0b0010000) + BLE_ABORT_ERROR = BLEPacketInfo(0b0001000) + BLE_HEADER_RECEIVED = BLEPacketInfo(0b0000100) + BLE_PACKET_RECEIVED = BLEPacketInfo(0b0000010) + BLE_PACKET_CRTL_BUSY = BLEPacketInfo(0b0000001) + + // FLRC Packet Info + FLRC_SYNC_ERROR = FLRCPacketInfo(0b1000000) + FLRC_LENGTH_ERROR = FLRCPacketInfo(0b0100000) + FLRC_CRC_ERROR = FLRCPacketInfo(0b0010000) + FLRC_ABORT_ERROR = FLRCPacketInfo(0b0001000) + FLRC_HEADER_RECEIVED = FLRCPacketInfo(0b0000100) + FLRC_PACKET_RECEIVED = FLRCPacketInfo(0b0000010) + FLRC_PACKET_CRTL_BUSY = FLRCPacketInfo(0b0000001) +) diff --git a/sx128x/errors.go b/sx128x/errors.go new file mode 100644 index 0000000..e81b812 --- /dev/null +++ b/sx128x/errors.go @@ -0,0 +1,19 @@ +package sx128x + +import "errors" + +var ( + ErrBusyPinTimeout = errors.New("busy pin timeout") + errDataTooLong = errors.New("data over 256 bytes") + errInvalidSleepConfig = errors.New("invalid sleep config") + errInvalidStandbyConfig = errors.New("invalid standby config") + errFrequencyTooLow = errors.New("frequency below 2.4Ghz") + errFrequencyTooHigh = errors.New("frequency above 2.5Ghz") + errPowerTooLow = errors.New("power level below -18dBm") + errPowerTooHigh = errors.New("power level above 13dBm") + errInvalidPeriodBase = errors.New("invalid period base") + errInvalidPacketType = errors.New("invalid packet type") + errInvalidRegulatorMode = errors.New("invalid regulator mode") + errPayloadLengthTooShort = errors.New("payload length too short") + errPayloadLengthTooLong = errors.New("payload length too long") +) diff --git a/sx128x/registers.go b/sx128x/registers.go new file mode 100644 index 0000000..83cd9db --- /dev/null +++ b/sx128x/registers.go @@ -0,0 +1,69 @@ +package sx128x + +const ( + + // SX128X register map + REG_FIRMWARE_VERSIONS = uint16(0x153) + REG_RX_GAIN = uint16(0x891) + REG_MANUAL_GAIN_SETTING = uint16(0x895) + REG_LNA_GAIN_VALUE = uint16(0x89E) + REG_LNA_GAIN_CONTROL = uint16(0x89F) + REG_SYNCH_PEAK_ATTENUATION = uint16(0x8C2) + REG_PAYLOAD_LENGTH = uint16(0x901) + REG_LORA_HEADER_MODE = uint16(0x903) + REG_RANGING_REQUEST_ADDRESS_BYTE_3 = uint16(0x912) + REG_RANGING_REQUEST_ADDRESS_BYTE_2 = uint16(0x913) + REG_RANGING_REQUEST_ADDRESS_BYTE_1 = uint16(0x914) + REG_RANGING_REQUEST_ADDRESS_BYTE_0 = uint16(0x915) + REG_RANGING_DEVICE_ADDRESS_BYTE_3 = uint16(0x916) + REG_RANGING_DEVICE_ADDRESS_BYTE_2 = uint16(0x917) + REG_RANGING_DEVICE_ADDRESS_BYTE_1 = uint16(0x918) + REG_RANGING_DEVICE_ADDRESS_BYTE_0 = uint16(0x919) + REG_RANGING_FILTER_WINDOW_SIZE = uint16(0x91E) + REG_RESET_RANGING_FILTER = uint16(0x923) + REG_RANGING_RESULT_MUX = uint16(0x924) + REG_SF_ADDITIONAL_CONFIGURATION = uint16(0x925) + REG_RANGING_CALIBRATION_BYTE_2 = uint16(0x92B) + REG_RANGING_CALIBRATION_BYTE_1 = uint16(0x92C) + REG_RANGING_CALIBRATION_BYTE_0 = uint16(0x92D) + REG_RANGING_ID_CHECK_LENGTH = uint16(0x931) + REG_FREQUENCY_ERROR_CORRECTION = uint16(0x93C) + REG_CAD_DETECT_PEAK = uint16(0x942) + REG_LORA_SYNC_WORD_MSB = uint16(0x944) + REG_LORA_SYNC_WORD_LSB = uint16(0x945) + REG_HEADER_CRC = uint16(0x954) + REG_CODING_RATE = uint16(0x950) + REG_FEI_BYTE_2 = uint16(0x954) + REG_FEI_BYTE_1 = uint16(0x955) + REG_FEI_BYTE_0 = uint16(0x956) + REG_RANGING_RESULT_BYTE_2 = uint16(0x961) + REG_RANGING_RESULT_BYTE_1 = uint16(0x962) + REG_RANGING_RESULT_BYTE_0 = uint16(0x963) + REG_RANGING_RSSI = uint16(0x964) + REG_FREEZE_RANGING_RESULT = uint16(0x97F) + REG_PACKET_PREAMBLE_SETTINGS = uint16(0x9C1) + REG_WHITENING_INITIAL_VALUE = uint16(0x9C5) + REG_CRC_POLYNOMIAL_DEFINITION_MSB = uint16(0x9C6) + REG_CRC_POLYNOMIAL_DEFINITION_LSB = uint16(0x9C7) + REG_CRC_POLYNOMIAL_SEED_BYTE_2 = uint16(0x9C7) + REG_CRC_POLYNOMIAL_SEED_BYTE_1 = uint16(0x9C8) + REG_CRC_POLYNOMIAL_SEED_BYTE_0 = uint16(0x9C9) + REG_CRC_MSB_INITIAL_VALUE = uint16(0x9C8) + REG_CRC_LSB_INITIAL_VALUE = uint16(0x9C9) + REG_SYNC_ADDRESS_CONTROL = uint16(0x9CD) + REG_SYNC_ADDRESS_1_BYTE_4 = uint16(0x9CE) + REG_SYNC_ADDRESS_1_BYTE_3 = uint16(0x9CF) + REG_SYNC_ADDRESS_1_BYTE_2 = uint16(0x9D0) + REG_SYNC_ADDRESS_1_BYTE_1 = uint16(0x9D1) + REG_SYNC_ADDRESS_1_BYTE_0 = uint16(0x9D2) + REG_SYNC_ADDRESS_2_BYTE_4 = uint16(0x9D3) + REG_SYNC_ADDRESS_2_BYTE_3 = uint16(0x9D4) + REG_SYNC_ADDRESS_2_BYTE_2 = uint16(0x9D5) + REG_SYNC_ADDRESS_2_BYTE_1 = uint16(0x9D6) + REG_SYNC_ADDRESS_2_BYTE_0 = uint16(0x9D7) + REG_SYNC_ADDRESS_3_BYTE_4 = uint16(0x9D8) + REG_SYNC_ADDRESS_3_BYTE_3 = uint16(0x9D9) + REG_SYNC_ADDRESS_3_BYTE_2 = uint16(0x9DA) + REG_SYNC_ADDRESS_3_BYTE_1 = uint16(0x9DB) + REG_SYNC_ADDRESS_3_BYTE_0 = uint16(0x9DC) +) diff --git a/sx128x/sx128x.go b/sx128x/sx128x.go new file mode 100644 index 0000000..a477fc6 --- /dev/null +++ b/sx128x/sx128x.go @@ -0,0 +1,768 @@ +package sx128x + +import ( + "runtime" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/pin" +) + +type Device struct { + spi drivers.SPI + nssPin pin.Output + resetPin pin.Output + busyPin pin.Input + spiTxBuf []byte + spiRxBuf []byte +} + +func New(spi drivers.SPI, nssPin pin.Output, resetPin pin.Output, busyPin pin.Input) *Device { + return &Device{ + spi: spi, + nssPin: nssPin, + resetPin: resetPin, + busyPin: busyPin, + spiTxBuf: make([]byte, 256), // TODO: optimize buffer size + spiRxBuf: make([]byte, 256), + } +} + +func (d *Device) Reset() { + d.resetPin.Set(false) + time.Sleep(10 * time.Millisecond) + d.resetPin.Set(true) + time.Sleep(10 * time.Millisecond) +} + +func (d *Device) WaitWhileBusy(timeout time.Duration) error { + // largest busy period is on boot with around ~400ish this should be more than enough + now := time.Now() + for d.busyPin.Get() { + if time.Since(now) > timeout { + return ErrBusyPinTimeout + } + runtime.Gosched() + } + return nil +} + +// Get tranceiver status, returns circuit mode and command status +func (d *Device) GetStatus() (CircuitMode, CommandStatus, error) { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return 0, 0, err + } + d.nssPin.Set(false) + status, err := d.spi.Transfer(cmdGetStatus) + d.nssPin.Set(true) + + if err != nil { + return 0, 0, err + } + + circuitMode := (status & circuitModeMask) >> 5 + commandStatus := (status & commandStatusMask) >> 2 + return CircuitMode(circuitMode), CommandStatus(commandStatus), nil +} + +func (d *Device) WriteRegister(addr uint16, data []byte) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdWriteRegister, uint8((addr>>8)&0xFF), uint8(addr&0xFF)) + d.spiTxBuf = append(d.spiTxBuf, data...) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +func (d *Device) ReadRegister(addr uint16) (uint8, error) { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return 0, err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdReadRegister, uint8((addr&0xFF00)>>8), uint8(addr&0x00FF), 0x00, 0x00) + d.spiRxBuf = d.spiRxBuf[:5] + err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf) + d.nssPin.Set(true) + if err != nil { + return 0, err + } + return d.spiRxBuf[4], nil +} + +func (d *Device) WriteBuffer(offset uint8, data []byte) error { + if len(data) > 256 { + return errDataTooLong + } + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdWriteBuffer, offset) + d.spiTxBuf = append(d.spiTxBuf, data...) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Read data from the payload buffer starting at the given offset with the given length +func (d *Device) ReadBuffer(offset uint8, length uint8) ([]byte, error) { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return nil, err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdReadBuffer, offset, 0x00) + for i := uint8(0); i < length; i++ { + d.spiTxBuf = append(d.spiTxBuf, 0x00) + } + d.spiRxBuf = d.spiRxBuf[:len(d.spiTxBuf)] + err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf) + d.nssPin.Set(true) + if err != nil { + return nil, err + } + return d.spiRxBuf[3:], nil +} + +// Set the device into sleep mode with the given configuration: 0 (no retention), 1 (ram retentation), 2 (buffer retention) or 3 (ram and buffer retention) +func (d *Device) SetSleep(sleepConfig SleepConfig) error { + if sleepConfig > (SLEEP_DATA_BUFFER_RETAIN | SLEEP_DATA_RAM_RETAIN) { + return errInvalidSleepConfig + } + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetSleep, uint8(sleepConfig)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Put device into standby mode, 0 (RC) or 1 (XOSC) +func (d *Device) SetStandby(standbyConfig StandbyConfig) error { + if standbyConfig > STANDBY_XOSC { // XOSC is the highest standby config anything higher is invalid + return errInvalidStandbyConfig + } + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetStandby, uint8(standbyConfig)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Set the device into Frequency Synthesizer mode +func (d *Device) SetFs() error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetFS) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +func checkPeriodBase(periodBase PeriodBase) error { + if periodBase > PERIOD_BASE_4_MS { // 4ms is the highest period base anything higher is invalid + return errInvalidPeriodBase + } + return nil +} + +// Sets the device in transmit mode, the IRQ status should be cleared before using this command +// timout is determined by periodBase * periodBaseCount +func (d *Device) SetTx(periodBase PeriodBase, periodBaseCount uint16) error { + err := checkPeriodBase(periodBase) + if err != nil { + return err + } + err = d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetTx, uint8(periodBase), uint8((periodBaseCount>>8)&0xFF), uint8(periodBaseCount&0xFF)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Sets the device in receive mode, the IRQ status should be cleared before using this command +// timeout is determined by periodBase * periodBaseCount +func (d *Device) SetRx(periodBase PeriodBase, periodBaseCount uint16) error { + err := checkPeriodBase(periodBase) + if err != nil { + return err + } + err = d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetRx, uint8(periodBase), uint8((periodBaseCount>>8)&0xFF), uint8(periodBaseCount&0xFF)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Sets the device in a continuous receive mode, it enters receive mode with a timeout of periodBase * rxPeriodBaseCount. +// If no packet is received it will enter sleep mode for periodBase * sleepPeriodBaseCount before re-entering receive mode. +// The loop is exited when a packet is received or the device is put into standby mode. +func (d *Device) SetRxDutyCycle(periodBase PeriodBase, rxPeriodBaseCount uint16, sleepPeriodBaseCount uint16) error { + err := checkPeriodBase(periodBase) + if err != nil { + return err + } + err = d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetRxDutyCycle, uint8(periodBase), uint8((rxPeriodBaseCount&0xFF00)>>8), uint8(rxPeriodBaseCount&0x00FF)) + d.spiTxBuf = append(d.spiTxBuf, uint8((sleepPeriodBaseCount&0xFF00)>>8), uint8(sleepPeriodBaseCount&0x00FF)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Sets the transceiver into Long Preamble mode, and can only be used with either the LoRa mode and GFSK mode +func (d *Device) SetLongPreamble(enable bool) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetLongPreamble) + if enable { + d.spiTxBuf = append(d.spiTxBuf, 1) + } else { + d.spiTxBuf = append(d.spiTxBuf, 0) + } + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Channel activity detection (CAD) is a LoRa specific mode of operation where the device searches for a LoRa signal. +// After search has completed, the device returns to STDBY_RC mode. The length of the search is configured via the SetCadParams() command. +func (d *Device) SetCAD() error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetCAD) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Test command to generate a Continuous Wave (RF tone) at a selected frequency and output power +// The device remains in Tx Continuous Wave until the host sends a mode configuration command. +func (d *Device) SetTxContinuousWave() error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetTxContinuousWave) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Test command to generate an infinite sequence of alternating ‘0’s and ‘1’s in +// GFSK modulation and symbol 0 in LoRa. The device remains in transmit until the host sends a mode configuration command. +func (d *Device) SetTxContinuousPreamble() error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetContinuousPreamble) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// This command allows the transceiver to send a packet at a user programmable time after the end of a packet reception. +// This is useful for Bluetooth Low Energy (BLE) compatibility which requires the transceiver to be able to send back a response 150µs after a packet reception. +func (d *Device) SetAutoTx(timeUs uint16) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetAutoTx, uint8((timeUs&0xFF00)>>8), uint8(timeUs&0x00FF)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Modifies the chip behavior so that the state following a Rx or Tx operation is FS and not standby. +// This allows for faster transitions between Rx and/or Tx. +func (d *Device) SetAutoFs(enable bool) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetAutoFS) + if enable { + d.spiTxBuf = append(d.spiTxBuf, 1) + } else { + d.spiTxBuf = append(d.spiTxBuf, 0) + } + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Choose between GFSK, LoRa, Ranging, FLRC or BLE packet types, this will affect the available configuration parameters and the structure of the packet +func (d *Device) SetPacketType(packetType PacketType) error { + if packetType > PACKET_TYPE_BLE { // BLE is the highest packet type anything higher is invalid. + return errInvalidPacketType + } + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetPacketType, uint8(packetType)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Get the currently configured packet type, this will be 0 (GFSK), 1 (LoRa), 2 (Ranging), 3 (FLRC) or 4 (BLE) +func (d *Device) GetPacketType() (PacketType, error) { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return 0, err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdGetPacketType, 0x00, 0x00) + d.spiRxBuf = d.spiRxBuf[:3] + err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf) + d.nssPin.Set(true) + if err != nil { + return 0, err + } + return PacketType(d.spiRxBuf[2]), nil +} + +// Set the RF frequency in Hz, must be between 2.4 GHz and 2.5 GHz +func (d *Device) SetRfFrequency(frequencyHz uint32) error { + if frequencyHz < 2400000000 { + return errFrequencyTooLow + } + if frequencyHz > 2500000000 { + return errFrequencyTooHigh + } + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + rfFrequency := uint32((uint64(frequencyHz) << 18) / 52000000) + d.spiTxBuf = append(d.spiTxBuf, cmdSetRFFrequency, uint8((rfFrequency>>16)&0xFF), uint8((rfFrequency>>8)&0xFF), uint8(rfFrequency&0xFF)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Set the output power in dBm, must be between -18 and 13 dBm, and the ramp time +func (d *Device) SetTxParams(powerdBm int8, rampTime RadioRampTime) error { + if powerdBm < -18 { + return errPowerTooLow + } + if powerdBm > 13 { + return errPowerTooHigh + } + + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + adjustedPower := uint8(powerdBm + 18) + d.spiTxBuf = append(d.spiTxBuf, cmdSetTxParams, adjustedPower, uint8(rampTime)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Set the number of symbols used for channel activity detection which determines the sensitivity of the detection. +// This is only applicable in LoRa mode. +func (d *Device) SetCadParams(cadSymbolNum uint8) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetCADParams, cadSymbolNum) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Set the base address for the internal buffer for Tx and Rx operations. +// When transmitting or receiving data is read from or written to the buffer starting at the given offset. +func (d *Device) SetBufferBaseAddress(txBase uint8, rxBase uint8) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetBufferBaseAddress, txBase, rxBase) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// The arguments to this function depend on the packet type. It is recommended to use the mode specific functions for a better experience. +// BLE & GFSK: BitrateBandwidth, ModulationIndex, ModulationShaping +// FLRC: BitrateBandwidth, CodingRate, ModulationShaping +// LoRa & Ranging: SpreadingFactor, Bandwidth, CodingRate +func (d *Device) SetModulationParams(modParam1, modParam2, modParam3 uint8) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetModulationParams, modParam1, modParam2, modParam3) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +func (d *Device) SetModulationParamsBLE(bitrateBandwidth GFSKBLEBitrateBandwidth, modulationIndex ModulationIndex, modulationShaping ModulationShaping) error { + return d.SetModulationParams(uint8(bitrateBandwidth), uint8(modulationIndex), uint8(modulationShaping)) +} + +func (d *Device) SetModulationParamsGFSK(bitrateBandwidth GFSKBLEBitrateBandwidth, modulationIndex ModulationIndex, modulationShaping ModulationShaping) error { + return d.SetModulationParams(uint8(bitrateBandwidth), uint8(modulationIndex), uint8(modulationShaping)) +} + +func (d *Device) SetModulationParamsFLRC(bitrateBandwidth FLRCBitrateBandwidth, codingRate FLRCCodingRate, modulationShaping ModulationShaping) error { + return d.SetModulationParams(uint8(bitrateBandwidth), uint8(codingRate), uint8(modulationShaping)) +} + +func (d *Device) SetModulationParamsLoRa(spreadingFactor LoRaSpreadingFactor, bandwidth LoRaBandwidth, codingRate LoRaCodingRate) error { + return d.SetModulationParams(uint8(spreadingFactor), uint8(bandwidth), uint8(codingRate)) +} + +// The arguments to this function depend on the packet type. It is recommended to use the mode specific functions for a better experience. +// GFSK & FLRC: PreambleLength, SyncWordLength, SyncWordMatch, HeaderType, PayloadLength, CrcLength, Whitening +// BLE: ConnectionState, CrcLength, BleTestPayload, Whitening +// LoRa & Ranging: PreambleLength, HeaderType, PayloadLength, CRC, InvertIQ/chirp invert +func (d *Device) SetPacketParams(param1, param2, param3, param4, param5, param6, param7 uint8) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetPacketParams, param1, param2, param3, param4, param5, param6, param7) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Set GFSK related packet parameters, this assumes the packet type is already set to GFSK. +// - payloadLength: range of 0-255 +func (d *Device) SetPacketParamsGFSK(preambleLength GFSKPreambleLength, syncWordLength GFSKSyncWordLength, syncWordMatch GFSKSyncWordMatch, headerType GFSKHeaderType, payloadLength uint8, crcLength GFSKCrcType, whitening bool) error { + var whiteningVal uint8 + if whitening { + whiteningVal = whiteningEnable + } else { + whiteningVal = whiteningDisable + } + return d.SetPacketParams(uint8(preambleLength), uint8(syncWordLength), uint8(syncWordMatch), uint8(headerType), payloadLength, uint8(crcLength), whiteningVal) +} + +// Set FLRC related packet parameters, this assumes the packet type is already set to FLRC. +// - payloadLength: range of 6-127 +func (d *Device) SetPacketParamsFLRC(preambleLength FLRCPreambleLength, syncWordLength FLRCSyncWordLength, syncWordMatch FLRCSyncWordMatch, headerType FLRCHeaderType, payloadLength uint8, crcLength FLRCCrcType) error { + if payloadLength < 6 { + return errPayloadLengthTooShort + } + if payloadLength > 127 { + return errPayloadLengthTooLong + } + return d.SetPacketParams(uint8(preambleLength), uint8(syncWordLength), uint8(syncWordMatch), uint8(headerType), payloadLength, uint8(crcLength), whiteningDisable) +} + +// Set BLE related packet parameters, this assumes the packet type is already set to BLE. +func (d *Device) SetPacketParamsBLE(connectionState BLEConnectionState, crcLength BLECrcType, bleTestPayload BLETestPayload, whitening bool) error { + var whiteningVal uint8 + if whitening { + whiteningVal = whiteningEnable + } else { + whiteningVal = whiteningDisable + } + return d.SetPacketParams(uint8(connectionState), uint8(crcLength), uint8(bleTestPayload), whiteningVal, 0, 0, 0) +} + +// Set LoRa related packet parameters, this assumes the packet type is already set to LoRa. +// - payloadLength: range of 1-255 +func (d *Device) SetPacketParamsLoRa(preambleLength uint32, headerType LoRaHeaderType, payloadLength uint8, crcType LoRaCrcType, iqType LoRaIqType) error { + if payloadLength == 0 { + return errPayloadLengthTooShort + } + exponent, mantissa := getExponentAndMantissa(preambleLength) + return d.SetPacketParams(uint8(exponent<<4)|mantissa, uint8(headerType), payloadLength, uint8(crcType), uint8(iqType), 0, 0) +} + +func getExponentAndMantissa(value uint32) (uint8, uint8) { + // pulled from RadioLib https://github.com/jgromes/RadioLib/blob/master/src/modules/SX128x/SX128x.cpp + e := uint8(1) + m := uint8(1) + len := uint32(0) + for e = uint8(1); e <= 15; e++ { + for m = uint8(1); m <= 15; m++ { + len = uint32(m) * (uint32(1 << e)) + if len >= value { + break + } + } + if len >= value { + break + } + } + + return e, m +} + +// Get information about the most recent packet received. +// Return the payload length, the offset in the buffer where the payload starts. +func (d *Device) GetRxBufferStatus() (uint8, uint8, error) { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return 0, 0, err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdGetRxBufferStatus, 0x00, 0x00, 0x00) + d.spiRxBuf = d.spiRxBuf[:4] + err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf) + d.nssPin.Set(true) + if err != nil { + return 0, 0, err + } + return d.spiRxBuf[2], d.spiRxBuf[3], nil +} + +// The return type of this function depends on the packet type. Use mode specific function for typed returns. +// BLE, GFSK & FLRC: unused, rssiSync, errors, status, sync +// LoRa & Ranging: rssiSync, SNR +func (d *Device) GetPacketStatus() (uint8, uint8, uint8, uint8, uint8, error) { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return 0, 0, 0, 0, 0, err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdGetPacketStatus, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + d.spiRxBuf = d.spiRxBuf[:7] + err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf) + d.nssPin.Set(true) + if err != nil { + return 0, 0, 0, 0, 0, err + } + return d.spiRxBuf[2], d.spiRxBuf[3], d.spiRxBuf[4], d.spiRxBuf[5], d.spiRxBuf[6], nil +} + +// Get information about the most recent GFSK packet received or transmitted: +// - RSSI of last received packet +// - packet information (each bit represents a different error or status flag) +// - whether the last packet transmission has ended +// - the sync word that was used for the last packet reception (0-3) +func (d *Device) GetPacketStatusGFSK() (float32, GFSKPacketInfo, bool, uint8, error) { + _, rssiSync, packetInfo, status, sync, err := d.GetPacketStatus() + if err != nil { + return 0, 0, false, 0, err + } + return float32(int8(rssiSync)) / 2 * -1, GFSKPacketInfo(packetInfo), status != 0, sync, nil +} + +// Get information about the most recent BLE packet received or transmitted: +// - RSSI of last received packet +// - packet information (each bit represents a different error or status flag) +// - whether the last packet transmission has ended +// - the sync word that was used for the last packet reception (0-1) +func (d *Device) GetPacketStatusBLE() (float32, BLEPacketInfo, bool, uint8, error) { + _, rssiSync, packetInfo, status, sync, err := d.GetPacketStatus() + if err != nil { + return 0, 0, false, 0, err + } + return float32(int8(rssiSync)) / 2 * -1, BLEPacketInfo(packetInfo), status != 0, sync, nil +} + +// Get information about the most recent BLE packet received or transmitted: +// - RSSI of last received packet +// - packet information (each bit represents a different error or status flag) +// - PID field of the received packet +// - NO_ACK field of the received packet +// - PID check status of the current packet +// - whether the last packet transmission has ended +// - the sync word that was used for the last packet reception (0-1) +func (d *Device) GetPacketStatusFLRC() (float32, FLRCPacketInfo, uint8, bool, bool, bool, uint8, error) { + _, rawRSSI, packetInfo, rxTxInfo, sync, err := d.GetPacketStatus() + + rxPid := (rxTxInfo & 0b11000000) >> 6 + noAck := (rxTxInfo & 0b00100000) != 0 + pidCheck := (rxTxInfo & 0b00010000) != 0 + txDone := (rxTxInfo & 0b00000001) != 0 + + if err != nil { + return 0, 0, 0, false, false, false, 0, err + } + return float32(int8(rawRSSI)) / 2 * -1, FLRCPacketInfo(packetInfo), rxPid, noAck, pidCheck, txDone, sync, nil +} + +// Get information about the most recent LoRa packet received: +// - RSSI of last received packet +// - signal-to-noise ratio (SNR) of last received packet +func (d *Device) GetPacketStatusLoRa() (float32, float32, error) { + rawRSSI, rawSnr, _, _, _, err := d.GetPacketStatus() + if err != nil { + return 0, 0, err + } + return float32(int8(rawRSSI)) / 2 * -1, float32(int8(rawSnr)) / 4, nil +} + +// Get the instantaneous RSSI value during reception of the packet +func (d *Device) GetRssiInst() (float32, error) { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return 0, err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdGetRSSIInst, 0x00, 0x00) + d.spiRxBuf = d.spiRxBuf[:3] + err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf) + d.nssPin.Set(true) + if err != nil { + return 0, err + } + return float32(int8(d.spiRxBuf[2])) / 2 * -1, nil +} + +// Configure the overall IRQ mask and the mapping of individual IRQs to the DIO1, DIO2 and DIO3 pins +func (d *Device) SetDioIrqParams(irqMask IRQMask, dio1Mask IRQMask, dio2Mask IRQMask, dio3Mask IRQMask) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetDIOIRQParams, uint8((irqMask&0xFF00)>>8), uint8(irqMask&0x00FF)) + d.spiTxBuf = append(d.spiTxBuf, uint8((dio1Mask&0xFF00)>>8), uint8(dio1Mask&0x00FF)) + d.spiTxBuf = append(d.spiTxBuf, uint8((dio2Mask&0xFF00)>>8), uint8(dio2Mask&0x00FF)) + d.spiTxBuf = append(d.spiTxBuf, uint8((dio3Mask&0xFF00)>>8), uint8(dio3Mask&0x00FF)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Get the current IRQ status. +func (d *Device) GetIrqStatus() (IRQMask, error) { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return 0, err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdGetIRQStatus, 0x00, 0x00, 0x00) + d.spiRxBuf = d.spiRxBuf[:4] + err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf) + d.nssPin.Set(true) + if err != nil { + return 0, err + } + return uint16(d.spiRxBuf[2])<<8 | uint16(d.spiRxBuf[3]), err +} + +// Clear the IRQ bits specified in the irqMask. +func (d *Device) ClearIrqStatus(irqMask IRQMask) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdClearIRQStatus, uint8((irqMask&0xFF00)>>8), uint8(irqMask&0x00FF)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Switch between the low-dropout regulator (LDO) and the DC-DC converter for internal power regulation. +func (d *Device) SetRegulatorMode(mode RegulatorMode) error { + if mode > REGULATOR_DC_DC { // DC-DC is the highest regulator mode anything higher is invalid + return errInvalidRegulatorMode + } + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetRegulatorMode, uint8(mode)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Stores the present context of the radio register values to the Data RAM which will be restored when the device wakes up from sleep mode. +func (d *Device) SetSaveContext() error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetSaveContext) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} From f459992f3cb87961e9356efc6db1b183914afe6d Mon Sep 17 00:00:00 2001 From: deadprogram Date: Wed, 6 May 2026 18:41:29 +0200 Subject: [PATCH 133/137] ws2812: add support for 160MHz cortex-m processors This adds hardware timing support for driving WS2812 LEDs on Cortex-M microcontrollers running at 160MHz (such as the STM32U585). - Updated `go:generate` directive to include 160MHz. - Generated the corresponding `ws2812_writeByte160` assembly routine. - Added a switch case in `WriteByte` to handle generic 160MHz processors. Signed-off-by: deadprogram --- ws2812/ws2812-asm_cortexm.go | 381 +++++++++++++++++++++++++++++++++++ ws2812/ws2812.go | 2 +- ws2812/ws2812_cortexm.go | 3 + 3 files changed, 385 insertions(+), 1 deletion(-) diff --git a/ws2812/ws2812-asm_cortexm.go b/ws2812/ws2812-asm_cortexm.go index 1f06c33..a1722f6 100644 --- a/ws2812/ws2812-asm_cortexm.go +++ b/ws2812/ws2812-asm_cortexm.go @@ -1281,6 +1281,377 @@ void ws2812_writeByte150(char c, uint32_t *portSet, uint32_t *portClear, uint32_ [portClear]"m"(*portClear)); } +__attribute__((always_inline)) +void ws2812_writeByte160(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) { + // Timings: + // T0H: 56 - 58 cycles or 350.0ns - 362.5ns + // T1H: 168 - 170 cycles or 1050.0ns - 1062.5ns + // TLD: 184 - cycles or 1150.0ns - + uint32_t value = (uint32_t)c << 24; + char i = 8; + __asm__ __volatile__( + "1: @ send_bit\n" + "\t str %[maskSet], %[portSet] @ [2] T0H and T0L start here\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t lsls %[value], #1 @ [1]\n" + "\t bcs.n 2f @ [1/3] skip_store\n" + "\t str %[maskClear], %[portClear] @ [2] T0H -> T0L transition\n" + "\t2: @ skip_store\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t str %[maskClear], %[portClear] @ [2] T1H -> T1L transition\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t subs %[i], #1 @ [1]\n" + "\t beq.n 3f @ [1/3] end\n" + "\t b 1b @ [1/3] send_bit\n" + "\t3: @ end\n" + : [value]"+r"(value), + [i]"+r"(i) + : [maskSet]"r"(maskSet), + [portSet]"m"(*portSet), + [maskClear]"r"(maskClear), + [portClear]"m"(*portClear)); +} + __attribute__((always_inline)) void ws2812_writeByte168(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) { // Timings: @@ -2192,6 +2563,16 @@ func (d Device) writeByte150(c byte) { interrupt.Restore(mask) } +func (d Device) writeByte160(c byte) { + portSet, maskSet := d.Pin.PortMaskSet() + portClear, maskClear := d.Pin.PortMaskClear() + + mask := interrupt.Disable() + C.ws2812_writeByte160(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear)) + + interrupt.Restore(mask) +} + func (d Device) writeByte168(c byte) { portSet, maskSet := d.Pin.PortMaskSet() portClear, maskClear := d.Pin.PortMaskClear() diff --git a/ws2812/ws2812.go b/ws2812/ws2812.go index 2066e03..9ca9695 100644 --- a/ws2812/ws2812.go +++ b/ws2812/ws2812.go @@ -4,7 +4,7 @@ // On RP2040/RP2350 it uses PIO for hardware-timed control. package ws2812 // import "tinygo.org/x/drivers/ws2812" -//go:generate go run gen-ws2812.go -arch=cortexm 16 48 64 120 125 150 168 200 +//go:generate go run gen-ws2812.go -arch=cortexm 16 48 64 120 125 150 160 168 200 //go:generate go run gen-ws2812.go -arch=tinygoriscv 160 320 import ( diff --git a/ws2812/ws2812_cortexm.go b/ws2812/ws2812_cortexm.go index b1f6ed2..44aa8de 100644 --- a/ws2812/ws2812_cortexm.go +++ b/ws2812/ws2812_cortexm.go @@ -34,6 +34,9 @@ func (d Device) WriteByte(c byte) error { case 150_000_000: // 150MHz, e.g. rp2350 d.writeByte150(c) return nil + case 160_000_000: // 160MHz, e.g. stm32u585 + d.writeByte160(c) + return nil case 168_000_000: // 168MHz, e.g. stm32f405 d.writeByte168(c) return nil From bb2d365868d81bc0000d07fffa3e5d30e365cd30 Mon Sep 17 00:00:00 2001 From: Joost Date: Sun, 17 May 2026 07:59:34 +0200 Subject: [PATCH 134/137] RP002-1.0.5 states that max TX power is 16 for eu868. Later, more subtle TX power tactics will be implemented. (#866) Co-authored-by: Joost Helberg --- lora/lorawan/region/eu868.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lora/lorawan/region/eu868.go b/lora/lorawan/region/eu868.go index 7eed1f4..c314d38 100644 --- a/lora/lorawan/region/eu868.go +++ b/lora/lorawan/region/eu868.go @@ -3,8 +3,8 @@ package region import "tinygo.org/x/drivers/lora" const ( - EU868_DEFAULT_PREAMBLE_LEN = 8 - EU868_DEFAULT_TX_POWER_DBM = 20 + EU868_DEFAULT_PREAMBLE_LEN = 8 // page 103 RP002-1.0.5 + EU868_DEFAULT_TX_POWER_DBM = 16 // page 36 RP002-1.0.5, 16 is the max ) type ChannelEU struct { From 04acd8e666bf78b072f543f01b9f423d1c020de1 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Thu, 23 Apr 2026 09:44:44 +0200 Subject: [PATCH 135/137] netlink: add Hostname field and some godocs comments Signed-off-by: deadprogram --- netlink/netlink.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/netlink/netlink.go b/netlink/netlink.go index aea60b4..4cff779 100644 --- a/netlink/netlink.go +++ b/netlink/netlink.go @@ -1,5 +1,4 @@ -// L2 data link layer - +// package netlink provides an interface for L2 data link layer operations. package netlink import ( @@ -20,6 +19,7 @@ var ( ErrNotSupported = errors.New("Not supported") ) +// Event is a network event type passed to the callback registered with NetNotify. type Event int // Network events @@ -38,6 +38,7 @@ const ( ConnectModeAP // Connect as Wifi Access Point ) +// AuthType is the type of WiFi authorization to use when connecting to an access point. type AuthType int // Wifi authorization types. Used when setting up an access point, or @@ -49,10 +50,11 @@ const ( AuthTypeWPA2Mixed // WPA2/WPA mixed authorization ) +// DefaultConnectTimeout is the default timeout for connection attempts. This is used when ConnectParams.ConnectTimeout is zero. const DefaultConnectTimeout = 10 * time.Second +// ConnectParams is the set of parameters used to connect a Netlinker device to a network. type ConnectParams struct { - // Connect mode ConnectMode @@ -81,22 +83,23 @@ type ConnectParams struct { // downed connection or hardware fault and try to recover the // connection. Set to zero to disable watchodog. WatchdogTimeout time.Duration + + // Hostname to use for this device. + Hostname string } // Netlinker is TinyGo's OSI L2 data link layer interface. Network device // drivers implement Netlinker to expose the device's L2 functionality. - type Netlinker interface { - - // Connect device to network + // NetConnect connects the device to a network NetConnect(params *ConnectParams) error - // Disconnect device from network + // NetDisconnect disconnects the device from the network NetDisconnect() - // Notify to register callback for network events + // NetNotify registers a callback for network events NetNotify(cb func(Event)) - // GetHardwareAddr returns device MAC address + // GetHardwareAddr returns the device's MAC address GetHardwareAddr() (net.HardwareAddr, error) } From 1d695a231aef8355dbb197405ef091235fab60b3 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sat, 7 Mar 2026 10:27:10 +0100 Subject: [PATCH 136/137] ws2812: add support for the ESP32-C3 processor This adds support to the WS2812 for the ESP32-C3 processor which is a RISC-V processor from Espressif. Signed-off-by: deadprogram --- examples/ws2812/others.go | 2 +- examples/ws2812/xiao-esp32c3.go | 11 + smoketest.sh | 1 + ws2812/gen-ws2812.go | 54 ++++- ws2812/ws2812-asm_esp32c3.go | 396 +++++++++++++++++++++++++++++++ ws2812/ws2812-asm_tinygoriscv.go | 384 +----------------------------- ws2812/ws2812.go | 3 +- ws2812/ws2812_esp32c3.go | 16 ++ ws2812/ws2812_tinygoriscv.go | 5 +- 9 files changed, 478 insertions(+), 394 deletions(-) create mode 100644 examples/ws2812/xiao-esp32c3.go create mode 100644 ws2812/ws2812-asm_esp32c3.go create mode 100644 ws2812/ws2812_esp32c3.go diff --git a/examples/ws2812/others.go b/examples/ws2812/others.go index b23153d..d68d57e 100644 --- a/examples/ws2812/others.go +++ b/examples/ws2812/others.go @@ -1,4 +1,4 @@ -//go:build !digispark && !arduino && !arduino_uno +//go:build !digispark && !arduino && !arduino_uno && !xiao_esp32c3 package main diff --git a/examples/ws2812/xiao-esp32c3.go b/examples/ws2812/xiao-esp32c3.go new file mode 100644 index 0000000..ad8f5a6 --- /dev/null +++ b/examples/ws2812/xiao-esp32c3.go @@ -0,0 +1,11 @@ +//go:build xiao_esp32c3 + +package main + +import "machine" + +func init() { + // Replace neo in the code below to match the pin + // that you are using if different. + neo = machine.D6 +} diff --git a/smoketest.sh b/smoketest.sh index 75316bb..2990485 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -93,6 +93,7 @@ tinygo build -size short -o ./build/test.bin -target=m5stamp-c3 ./examp tinygo build -size short -o ./build/test.hex -target=feather-nrf52840 ./examples/is31fl3731/main.go tinygo build -size short -o ./build/test.hex -target=arduino ./examples/ws2812 tinygo build -size short -o ./build/test.hex -target=digispark ./examples/ws2812 +tinygo build -size short -o ./build/test.bin -target=xiao-esp32c3 ./examples/ws2812 tinygo build -size short -o ./build/test.hex -target=trinket-m0 ./examples/bme280/main.go tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/microphone/main.go tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/buzzer/main.go diff --git a/ws2812/gen-ws2812.go b/ws2812/gen-ws2812.go index 54dc6b8..c1f2634 100644 --- a/ws2812/gen-ws2812.go +++ b/ws2812/gen-ws2812.go @@ -17,7 +17,8 @@ import ( // the new assembly implementation - no fiddly timings to calculate and no nops // to count! // -// Right now this is specific to Cortex-M chips and assume the following things: +// Right now this is specific to specific chips: +// On Cortex-M chips it assume the following things: // - Arithmetic operations (shift, add, sub) take up 1 clock cycle. // - The nop instruction also takes up 1 clock cycle. // - Store instructions (to the GPIO pins) take up 2 clock cycles. @@ -25,8 +26,15 @@ import ( // depends on whether the branch is taken or not. On the M4, the documentation // is less clear but it appears the instruction is still 1 to 3 cycles // (possibly including some branch prediction). -// It is certainly possible to extend this to other architectures, such as AVR -// and RISC-V if needed. +// On RISC-V chips it assumes the following things: +// - Arithmetic operations (shift, add, sub) take up 1 clock cycle. +// - The nop instruction also takes up 1 clock cycle. +// - Store instructions (to the GPIO pins) take up 1 clock cycle. +// - Branch instructions can take up 1 or 3 clock cycles, depending on branch +// prediction. This is based on the SiFive FE310 CPU, but hopefully it +// generalizes to other RISC-V chips as well. + +// It is certainly possible to extend this to other architectures, such as AVR as needed. // // Here are two important resources. For the timings: // https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/ @@ -45,6 +53,7 @@ type architectureImpl struct { maxBaseCyclesT1H int minBaseCyclesTLD int valueTemplate string // template for how to pass the 'c' byte to assembly + funcAttr string // C function attribute (default: always_inline) template string // assembly template } @@ -83,7 +92,7 @@ var architectures = map[string]architectureImpl{ // - branches are 1 or 3 cycles, depending on branch prediction // - ALU operations are 1 cycle (as on most CPUs) // Hopefully this generalizes to other chips. - buildTag: "tinygo.riscv32", + buildTag: "tinygo.riscv32 && !esp32c3", minBaseCyclesT0H: 1 + 1 + 1, // shift + branch (not taken) + store maxBaseCyclesT0H: 1 + 3 + 1, // shift + branch (not taken) + store minBaseCyclesT1H: 1 + 1 + 1, // shift + branch (taken) + store @@ -103,6 +112,37 @@ var architectures = map[string]architectureImpl{ @DELAY3 addi %[i], %[i], -1 // [1] bnez %[i], 1b // [1/3] send_bit +`, + }, + "esp32c3": { + // ESP32-C3 RISC-V core: + // - stores are 1 cycle + // - branches are 1 or 3 cycles + // - ALU operations are 1 cycle + // Uses the same instruction timing as the SiFive FE310, but the + // function is placed in IRAM instead of flash to avoid instruction + // cache miss stalls that would destroy WS2812 timing. + buildTag: "esp32c3", + minBaseCyclesT0H: 1 + 1 + 1, // shift + branch (not taken) + store + maxBaseCyclesT0H: 1 + 3 + 1, // shift + branch (not taken) + store + minBaseCyclesT1H: 1 + 1 + 1, // shift + branch (taken) + store + maxBaseCyclesT1H: 1 + 3 + 1, // shift + branch (taken) + store + minBaseCyclesTLD: 1 + 1 + 1, // subtraction + branch + store (in next cycle) + valueTemplate: "(uint32_t)c << 23", + funcAttr: `__attribute__((section(".iram1"), noinline))`, + template: ` +1: // send_bit + sw %[maskSet], %[portSet] // [1] T0H and T0L start here + @DELAY1 + slli %[value], %[value], 1 // [1] shift value left by 1 + bltz %[value], 2f // [1/3] skip_store + sw %[maskClear], %[portClear] // [1] T0H -> T0L transition +2: // skip_store + @DELAY2 + sw %[maskClear], %[portClear] // [1] T1H -> T1L transition + @DELAY3 + addi %[i], %[i], -1 // [1] + bnez %[i], 1b // [1/3] send_bit `, }, } @@ -208,7 +248,11 @@ func writeCAssembly(f *os.File, arch string, megahertz int) error { // ignore I/O errors. buf := &bytes.Buffer{} fmt.Fprintf(buf, "\n") - fmt.Fprintf(buf, "__attribute__((always_inline))\nvoid ws2812_writeByte%d(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {\n", megahertz) + funcAttr := archImpl.funcAttr + if funcAttr == "" { + funcAttr = "__attribute__((always_inline))" + } + fmt.Fprintf(buf, "%s\nvoid ws2812_writeByte%d(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {\n", funcAttr, megahertz) fmt.Fprintf(buf, " // Timings:\n") fmt.Fprintf(buf, " // T0H: %2d - %2d cycles or %.1fns - %.1fns\n", actualMinCyclesT0H, actualMaxCyclesT0H, actualMinNanosecondsT0H, actualMaxNanosecondsT0H) fmt.Fprintf(buf, " // T1H: %2d - %2d cycles or %.1fns - %.1fns\n", actualMinCyclesT1H, actualMaxCyclesT1H, actualMinNanosecondsT1H, actualMaxNanosecondsT1H) diff --git a/ws2812/ws2812-asm_esp32c3.go b/ws2812/ws2812-asm_esp32c3.go new file mode 100644 index 0000000..21e3659 --- /dev/null +++ b/ws2812/ws2812-asm_esp32c3.go @@ -0,0 +1,396 @@ +//go:build esp32c3 + +package ws2812 + +// Warning: autogenerated file. Instead of modifying this file, change +// gen-ws2812.go and run "go generate". + +import "runtime/interrupt" +import "unsafe" + +/* +#include + +__attribute__((section(".iram1"), noinline)) +void ws2812_writeByte160(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) { + // Timings: + // T0H: 56 - 58 cycles or 350.0ns - 362.5ns + // T1H: 168 - 170 cycles or 1050.0ns - 1062.5ns + // TLD: 184 - cycles or 1150.0ns - + uint32_t value = (uint32_t)c << 23; + char i = 8; + __asm__ __volatile__( + "1: // send_bit\n" + "\t sw %[maskSet], %[portSet] // [1] T0H and T0L start here\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t slli %[value], %[value], 1 // [1] shift value left by 1\n" + "\t bltz %[value], 2f // [1/3] skip_store\n" + "\t sw %[maskClear], %[portClear] // [1] T0H -> T0L transition\n" + "\t2: // skip_store\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t sw %[maskClear], %[portClear] // [1] T1H -> T1L transition\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t addi %[i], %[i], -1 // [1]\n" + "\t bnez %[i], 1b // [1/3] send_bit\n" + : [value]"+r"(value), + [i]"+r"(i) + : [maskSet]"r"(maskSet), + [portSet]"m"(*portSet), + [maskClear]"r"(maskClear), + [portClear]"m"(*portClear)); +} +*/ +import "C" + +func (d Device) writeByte160(c byte) { + portSet, maskSet := d.Pin.PortMaskSet() + portClear, maskClear := d.Pin.PortMaskClear() + + mask := interrupt.Disable() + C.ws2812_writeByte160(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear)) + + interrupt.Restore(mask) +} diff --git a/ws2812/ws2812-asm_tinygoriscv.go b/ws2812/ws2812-asm_tinygoriscv.go index 05e9653..046df7a 100644 --- a/ws2812/ws2812-asm_tinygoriscv.go +++ b/ws2812/ws2812-asm_tinygoriscv.go @@ -1,4 +1,4 @@ -//go:build tinygo.riscv32 +//go:build tinygo.riscv32 && !esp32c3 package ws2812 @@ -11,378 +11,6 @@ import "unsafe" /* #include -__attribute__((always_inline)) -void ws2812_writeByte160(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) { - // Timings: - // T0H: 56 - 58 cycles or 350.0ns - 362.5ns - // T1H: 168 - 170 cycles or 1050.0ns - 1062.5ns - // TLD: 184 - cycles or 1150.0ns - - uint32_t value = (uint32_t)c << 23; - char i = 8; - __asm__ __volatile__( - "1: // send_bit\n" - "\t sw %[maskSet], %[portSet] // [1] T0H and T0L start here\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t slli %[value], %[value], 1 // [1] shift value left by 1\n" - "\t bltz %[value], 2f // [1/3] skip_store\n" - "\t sw %[maskClear], %[portClear] // [1] T0H -> T0L transition\n" - "\t2: // skip_store\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t sw %[maskClear], %[portClear] // [1] T1H -> T1L transition\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t addi %[i], %[i], -1 // [1]\n" - "\t bnez %[i], 1b // [1/3] send_bit\n" - : [value]"+r"(value), - [i]"+r"(i) - : [maskSet]"r"(maskSet), - [portSet]"m"(*portSet), - [maskClear]"r"(maskClear), - [portClear]"m"(*portClear)); -} - __attribute__((always_inline)) void ws2812_writeByte320(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) { // Timings: @@ -1109,16 +737,6 @@ void ws2812_writeByte320(char c, uint32_t *portSet, uint32_t *portClear, uint32_ */ import "C" -func (d Device) writeByte160(c byte) { - portSet, maskSet := d.Pin.PortMaskSet() - portClear, maskClear := d.Pin.PortMaskClear() - - mask := interrupt.Disable() - C.ws2812_writeByte160(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear)) - - interrupt.Restore(mask) -} - func (d Device) writeByte320(c byte) { portSet, maskSet := d.Pin.PortMaskSet() portClear, maskClear := d.Pin.PortMaskClear() diff --git a/ws2812/ws2812.go b/ws2812/ws2812.go index 9ca9695..eaa26ab 100644 --- a/ws2812/ws2812.go +++ b/ws2812/ws2812.go @@ -5,7 +5,8 @@ package ws2812 // import "tinygo.org/x/drivers/ws2812" //go:generate go run gen-ws2812.go -arch=cortexm 16 48 64 120 125 150 160 168 200 -//go:generate go run gen-ws2812.go -arch=tinygoriscv 160 320 +//go:generate go run gen-ws2812.go -arch=tinygoriscv 320 +//go:generate go run gen-ws2812.go -arch=esp32c3 160 import ( "errors" diff --git a/ws2812/ws2812_esp32c3.go b/ws2812/ws2812_esp32c3.go new file mode 100644 index 0000000..d67c761 --- /dev/null +++ b/ws2812/ws2812_esp32c3.go @@ -0,0 +1,16 @@ +//go:build esp32c3 + +package ws2812 + +import "machine" + +// Send a single byte using the WS2812 protocol. +func (d Device) WriteByte(c byte) error { + switch machine.CPUFrequency() { + case 160_000_000: // 160MHz + d.writeByte160(c) + return nil + default: + return errUnknownClockSpeed + } +} diff --git a/ws2812/ws2812_tinygoriscv.go b/ws2812/ws2812_tinygoriscv.go index 7212310..735cdac 100644 --- a/ws2812/ws2812_tinygoriscv.go +++ b/ws2812/ws2812_tinygoriscv.go @@ -1,4 +1,4 @@ -//go:build tinygo.riscv32 +//go:build tinygo.riscv32 && !esp32c3 package ws2812 @@ -7,9 +7,6 @@ import "machine" // Send a single byte using the WS2812 protocol. func (d Device) WriteByte(c byte) error { switch machine.CPUFrequency() { - case 160_000_000: // 160MHz, e.g. esp32c3 - d.writeByte160(c) - return nil case 320_000_000: // 320MHz, e.g. fe310 d.writeByte320(c) return nil From dc6edbb694a3e8e7ebccea427cfe94c1f5c8d5a8 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Fri, 12 Jun 2026 19:00:09 +0200 Subject: [PATCH 137/137] ws2812: fix PIO TX FIFO overflow on rp2040/rp2350 dropping LEDs PutRGB calls TxPut which is non-blocking and silently discards data when the TX FIFO is full. Wait for FIFO space before each PutRGB using runtime.Gosched() to yield cooperatively, matching the pattern used by piolib.WriteRaw. Signed-off-by: deadprogram --- ws2812/ws2812_rp2_pio.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ws2812/ws2812_rp2_pio.go b/ws2812/ws2812_rp2_pio.go index 2cc29e1..ac142bd 100644 --- a/ws2812/ws2812_rp2_pio.go +++ b/ws2812/ws2812_rp2_pio.go @@ -5,6 +5,7 @@ package ws2812 import ( "image/color" "machine" + "runtime" pio "github.com/tinygo-org/pio/rp2-pio" "github.com/tinygo-org/pio/rp2-pio/piolib" @@ -27,6 +28,9 @@ func newWS2812Device(pin machine.Pin) Device { writeColorFunc: func(_ Device, buf []color.RGBA, brightness uint8) error { for _, c := range buf { r, g, b := applyBrightness(c, brightness) + for ws.IsQueueFull() { + runtime.Gosched() + } ws.PutRGB(r, g, b) } return nil