Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
module github.com/cafedomingo/SKU_RM0004

go 1.26.0

require (
periph.io/x/conn/v3 v3.7.3
periph.io/x/host/v3 v3.8.5
)
6 changes: 0 additions & 6 deletions go.sum

This file was deleted.

67 changes: 43 additions & 24 deletions internal/st7735/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ package st7735
import (
"encoding/binary"
"fmt"
"io"
"log/slog"
"os"
"syscall"
"time"

"periph.io/x/conn/v3/i2c"
"periph.io/x/conn/v3/i2c/i2creg"
"periph.io/x/host/v3"
)

const (
i2cBus = "/dev/i2c-1"
i2cBusPath = "/dev/i2c-1"
i2cAddress = 0x18
burstMaxLen = 160 // hardware limit, do NOT increase
burstDelayUS = 450 // empirically tuned at 400kHz
yOffset = 24 // controller is 160x160, our 160x80 starts at row 24
i2cSlave = 0x0703 // I2C_SLAVE ioctl from <linux/i2c-dev.h>
burstMaxLen = 160 // hardware limit, do NOT increase
burstDelayUS = 450 // empirically tuned at 400kHz
yOffset = 24 // controller is 160x160, our 160x80 starts at row 24

regWriteData = 0x00
regBurstWrite = 0x01
Expand All @@ -33,32 +33,51 @@ type Display interface {
Close() error
}

// i2cConn talks to an I2C device through the kernel's i2c-dev interface:
// one I2C_SLAVE ioctl to latch the address, then plain write(2) for each
// transaction.
type i2cConn struct {
f *os.File
}

func openI2C(path string, addr uint8) (*i2cConn, error) {
f, err := os.OpenFile(path, os.O_RDWR, 0)
if err != nil {
return nil, err
}
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), i2cSlave, uintptr(addr)); errno != 0 {
_ = f.Close()
return nil, fmt.Errorf("ioctl I2C_SLAVE 0x%02x: %w", addr, errno)
}
return &i2cConn{f: f}, nil
}

func (c *i2cConn) Write(p []byte) (int, error) {
return c.f.Write(p)
}

func (c *i2cConn) Close() error {
return c.f.Close()
}

type display struct {
dev *i2c.Dev
bus i2c.BusCloser
dev io.WriteCloser
logger *slog.Logger
}

// NewDisplay initializes the I2C bus and returns a Display backed by the
// NewDisplay opens the I2C bus and returns a Display backed by the
// UCTRONICS SKU_RM0004 ST7735 controller at address 0x18.
func NewDisplay(logger *slog.Logger) (Display, error) {
if _, err := host.Init(); err != nil {
return nil, fmt.Errorf("st7735: host init: %w", err)
}

bus, err := i2creg.Open(i2cBus)
dev, err := openI2C(i2cBusPath, i2cAddress)
if err != nil {
return nil, fmt.Errorf("st7735: open i2c bus %s: %w", i2cBus, err)
return nil, fmt.Errorf("st7735: open i2c bus %s: %w", i2cBusPath, err)
}

dev := &i2c.Dev{Bus: bus, Addr: i2cAddress}

return &display{dev: dev, bus: bus, logger: logger}, nil
return &display{dev: dev, logger: logger}, nil
}

// writeCommand sends a 3-byte I2C command: [register, high, low].
func (d *display) writeCommand(reg, hi, lo byte) {
if err := d.dev.Tx([]byte{reg, hi, lo}, nil); err != nil {
if _, err := d.dev.Write([]byte{reg, hi, lo}); err != nil {
d.logger.Warn("i2c write failed", "register", reg, "error", err)
}
}
Expand Down Expand Up @@ -90,7 +109,7 @@ func (d *display) burstSend(data []byte) {
if chunk > burstMaxLen {
chunk = burstMaxLen
}
if err := d.dev.Tx(data[offset:offset+chunk], nil); err != nil {
if _, err := d.dev.Write(data[offset : offset+chunk]); err != nil {
d.logger.Warn("burst send failed", "offset", offset, "error", err)
}
offset += chunk
Expand Down Expand Up @@ -125,5 +144,5 @@ func (d *display) SendFull(pixels []uint16) {

// Close releases the I2C bus.
func (d *display) Close() error {
return d.bus.Close()
return d.dev.Close()
}
94 changes: 94 additions & 0 deletions internal/st7735/driver_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,103 @@
package st7735

import (
"log/slog"
"testing"
)

// fakeI2C records every write so tests can check protocol framing.
type fakeI2C struct {
writes [][]byte
closed bool
}

func (f *fakeI2C) Write(p []byte) (int, error) {
cp := make([]byte, len(p))
copy(cp, p)
f.writes = append(f.writes, cp)
return len(p), nil
}

func (f *fakeI2C) Close() error {
f.closed = true
return nil
}

func TestSendRegionFraming(t *testing.T) {
fake := &fakeI2C{}
d := &display{dev: fake, logger: slog.Default()}

// Two rows of 160 pixels starting at y=10: 640 bytes of pixel data,
// which must be split into 160-byte burst chunks.
pixels := make([]uint16, 2*Width)
d.SendRegion(0, 10, Width, 2, pixels)

want := [][]byte{
{regXCoord, 0, Width - 1}, // column window
{regYCoord, 10 + yOffset, 11 + yOffset}, // row window with panel offset
{regCharData, 0x00, 0x00},
{regSync, 0x00, 0x01},
{regBurstWrite, 0x00, 0x01}, // burst on
}
if len(fake.writes) < len(want) {
t.Fatalf("got %d writes, want at least %d", len(fake.writes), len(want))
}
for i, w := range want {
got := fake.writes[i]
if len(got) != len(w) {
t.Fatalf("write[%d] = %v, want %v", i, got, w)
}
for j := range w {
if got[j] != w[j] {
t.Errorf("write[%d][%d] = 0x%02X, want 0x%02X", i, j, got[j], w[j])
}
}
}

// Two rows × 160 pixels × 2 bytes = 640 bytes → exactly 4 burst chunks.
wantBurstCount := 4
if got, want := len(fake.writes), len(want)+wantBurstCount+2; got != want {
t.Fatalf("got %d writes, want %d", got, want)
}

// Pixel bursts: every chunk must respect the hardware limit and the
// total must equal the region's byte size.
burst := fake.writes[len(want) : len(want)+wantBurstCount]
total := 0
for i, chunk := range burst {
if len(chunk) > burstMaxLen {
t.Errorf("burst chunk %d is %d bytes, exceeds hardware limit %d", i, len(chunk), burstMaxLen)
}
total += len(chunk)
}
if total != 2*Width*2 {
t.Errorf("burst total = %d bytes, want %d", total, 2*Width*2)
}

// Trailing writes: burst off, then sync.
wantTail := [][]byte{
{regBurstWrite, 0x00, 0x00},
{regSync, 0x00, 0x01},
}
last := fake.writes[len(want)+wantBurstCount:]
for i, w := range wantTail {
if got := last[i]; len(got) != len(w) || got[0] != w[0] || got[1] != w[1] || got[2] != w[2] {
t.Errorf("trailing write[%d] = %v, want %v", i, got, w)
}
}
}

func TestDisplayClose(t *testing.T) {
fake := &fakeI2C{}
d := &display{dev: fake, logger: slog.Default()}
if err := d.Close(); err != nil {
t.Fatalf("Close() = %v", err)
}
if !fake.closed {
t.Error("Close() did not close the underlying bus")
}
}

func TestPixelsToBytes(t *testing.T) {
tests := []struct {
name string
Expand Down