diff --git a/go.mod b/go.mod index d15e812..50bb4dc 100644 --- a/go.mod +++ b/go.mod @@ -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 -) diff --git a/go.sum b/go.sum deleted file mode 100644 index fd46385..0000000 --- a/go.sum +++ /dev/null @@ -1,6 +0,0 @@ -github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= -github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= -periph.io/x/conn/v3 v3.7.3 h1:+8UblkC4omTB1M+jZTvTj3qoxQOTJy0ZRQm8DLUuVzc= -periph.io/x/conn/v3 v3.7.3/go.mod h1:tyV9YaYquOJ2Q2yAL0B5zk9ZvHGsbW56M6y92wjyPDQ= -periph.io/x/host/v3 v3.8.5 h1:g4g5xE1XZtDiGl1UAJaUur1aT7uNiFLMkyMEiZ7IHII= -periph.io/x/host/v3 v3.8.5/go.mod h1:hPq8dISZIc+UNfWoRj+bPH3XEBQqJPdFdx218W92mdc= diff --git a/internal/st7735/driver.go b/internal/st7735/driver.go index 9981d32..c81d5a0 100644 --- a/internal/st7735/driver.go +++ b/internal/st7735/driver.go @@ -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 + 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 @@ -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) } } @@ -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 @@ -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() } diff --git a/internal/st7735/driver_test.go b/internal/st7735/driver_test.go index db5cdef..ed7b013 100644 --- a/internal/st7735/driver_test.go +++ b/internal/st7735/driver_test.go @@ -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