diff --git a/seesaw/README.MD b/seesaw/README.MD new file mode 100644 index 0000000..0ce39bb --- /dev/null +++ b/seesaw/README.MD @@ -0,0 +1,9 @@ +# Seesaw Driver + +This is a basic driver to interact with Adafruit's seesaw board. + +It exposes constants for the documented modules as well as their functions. Sub-packages +may contain more refined drivers based on those. + +- [Device Overview](https://learn.adafruit.com/adafruit-seesaw-atsamd09-breakout/overview) +- [Adafruit Datasheet](https://cdn-learn.adafruit.com/downloads/pdf/adafruit-seesaw-atsamd09-breakout.pdf) diff --git a/seesaw/mocki2c_test.go b/seesaw/mocki2c_test.go new file mode 100644 index 0000000..28d897e --- /dev/null +++ b/seesaw/mocki2c_test.go @@ -0,0 +1,48 @@ +package seesaw + +import ( + "encoding/hex" + "fmt" + "testing" +) + +type I2CHandleFunc func(t *testing.T, w, r []byte) error + +// mocki2c implements the drivers.I2C interface and matches a list +// of handlers against actual invocations. Useful to test command/reply style I2C devices. +type mocki2c struct { + addr uint16 + handlers []I2CHandleFunc + t *testing.T +} + +func (m *mocki2c) Tx(addr uint16, w, r []byte) error { + assertEquals(m.t, addr, m.addr) + if len(m.handlers) == 0 { + ws := hex.EncodeToString(w) + rs := hex.EncodeToString(r) + panic(fmt.Sprintf("no handlers for: addr='%02x' w='%s' r='%s'", byte(addr), ws, rs)) + } + h := m.handlers[0] + m.handlers = m.handlers[1:] + return h(m.t, w, r) +} + +func newMockDev(t *testing.T, addr uint16, handlers ...I2CHandleFunc) *mocki2c { + return &mocki2c{ + addr: addr, + handlers: handlers, + t: t, + } +} + +func when(expectedWrite, returningRead []byte, returningError error) I2CHandleFunc { + return func(t *testing.T, w, r []byte) error { + assertEquals(t, w, expectedWrite) + assertEquals(t, len(r), len(returningRead)) + if r != nil { + copy(r, returningRead) + } + return returningError + } +} diff --git a/seesaw/registers.go b/seesaw/registers.go new file mode 100644 index 0000000..27e6105 --- /dev/null +++ b/seesaw/registers.go @@ -0,0 +1,102 @@ +// Package seesaw provides a driver for the "i2c to whathever interface" called seesaw +// documentation: https://learn.adafruit.com/adafruit-seesaw-atsamd09-breakout/overview +package seesaw + +type ModuleBaseAddress byte + +// Module Base Addreses +// The module base addresses for different seesaw modules. +const ( + ModuleStatusBase ModuleBaseAddress = 0x00 + ModuleGpioBase ModuleBaseAddress = 0x01 + ModuleSercom0Base ModuleBaseAddress = 0x02 + + ModuleTimerBase ModuleBaseAddress = 0x08 + ModuleAdcBase ModuleBaseAddress = 0x09 + ModuleDacBase ModuleBaseAddress = 0x0A + ModuleInterruptBase ModuleBaseAddress = 0x0B + ModuleDapBase ModuleBaseAddress = 0x0C + ModuleEepromBase ModuleBaseAddress = 0x0D + ModuleNeoPixelBase ModuleBaseAddress = 0x0E + ModuleTouchBase ModuleBaseAddress = 0x0F + ModuleKeypadBase ModuleBaseAddress = 0x10 + ModuleEncoderBase ModuleBaseAddress = 0x11 + ModuleSpectrumBase ModuleBaseAddress = 0x12 +) + +type FunctionAddress byte + +// GPIO module function address registers +const ( + FunctionGpioDirsetBulk FunctionAddress = 0x02 + FunctionGpioDirclrBulk FunctionAddress = 0x03 + FunctionGpioBulk FunctionAddress = 0x04 + FunctionGpioBulkSet FunctionAddress = 0x05 + FunctionGpioBulkClr FunctionAddress = 0x06 + FunctionGpioBulkToggle FunctionAddress = 0x07 + FunctionGpioIntenset FunctionAddress = 0x08 + FunctionGpioIntenclr FunctionAddress = 0x09 + FunctionGpioIntflag FunctionAddress = 0x0A + FunctionGpioPullenset FunctionAddress = 0x0B + FunctionGpioPullenclr FunctionAddress = 0x0C +) + +// status module function address registers +const ( + FunctionStatusHwId FunctionAddress = 0x01 + FunctionStatusVersion FunctionAddress = 0x02 + FunctionStatusOptions FunctionAddress = 0x03 + FunctionStatusTemp FunctionAddress = 0x04 + FunctionStatusSwrst FunctionAddress = 0x7F +) + +// timer module function address registers +const ( + FunctionTimerStatus FunctionAddress = 0x00 + FunctionTimerPwm FunctionAddress = 0x01 + FunctionTimerFreq FunctionAddress = 0x02 +) + +// ADC module function address registers +const ( + FunctionAdcStatus FunctionAddress = 0x00 + FunctionAdcInten FunctionAddress = 0x02 + FunctionAdcIntenclr FunctionAddress = 0x03 + FunctionAdcWinmode FunctionAddress = 0x04 + FunctionAdcWinthresh FunctionAddress = 0x05 + FunctionAdcChannelOffset FunctionAddress = 0x07 +) + +// Sercom module function address registers +const ( + FunctionSercomStatus FunctionAddress = 0x00 + FunctionSercomInten FunctionAddress = 0x02 + FunctionSercomIntenclr FunctionAddress = 0x03 + FunctionSercomBaud FunctionAddress = 0x04 + FunctionSercomData FunctionAddress = 0x05 +) + +// neopixel module function address registers +const ( + FunctionNeopixelStatus FunctionAddress = 0x00 + FunctionNeopixelPin FunctionAddress = 0x01 + FunctionNeopixelSpeed FunctionAddress = 0x02 + FunctionNeopixelBufLength FunctionAddress = 0x03 + FunctionNeopixelBuf FunctionAddress = 0x04 + FunctionNeopixelShow FunctionAddress = 0x05 +) + +// touch module function address registers +const ( + FunctionTouchChannelOffset FunctionAddress = 0x10 +) + +// keypad module function address registers +const ( + FunctionKeypadStatus FunctionAddress = 0x00 + FunctionKeypadEvent FunctionAddress = 0x01 + FunctionKeypadIntenset FunctionAddress = 0x02 + FunctionKeypadIntenclr FunctionAddress = 0x03 + FunctionKeypadCount FunctionAddress = 0x04 + FunctionKeypadFifo FunctionAddress = 0x10 +) diff --git a/seesaw/seesaw.go b/seesaw/seesaw.go new file mode 100644 index 0000000..b5ed8ce --- /dev/null +++ b/seesaw/seesaw.go @@ -0,0 +1,130 @@ +// Package seesaw provides a driver implementation to communicate with Adafruit's seesaw chip. +// There are many Adafruit boards that use a seesaw. Moisture sensors, LED keyboards, ... +package seesaw + +import ( + "errors" + "strconv" + "time" + "tinygo.org/x/drivers" +) + +const DefaultAddress = 0x49 + +// empirically determined standardDelay, the one from the official library seems to be too short (250us) +const defaultDelay = 100 * time.Millisecond + +const ( + seesawHwIdCodeSAMD09 = 0x55 // HW ID code for SAMD09 + seesawHwIdCodeTINY8x7 = 0x87 // HW ID code for ATtiny817 +) + +type Seesaw interface { + + // Read reads a number of bytes from the device after sending the read command and waiting 'delay'. The delays depend + // on the module and function and are documented in the seesaw datasheet + Read(module ModuleBaseAddress, function FunctionAddress, buf []byte, delay time.Duration) error + + // Write writes an entire array into a given module and function + Write(module ModuleBaseAddress, function FunctionAddress, buf []byte) error +} + +type Device struct { + bus drivers.I2C + Address uint16 + standardDelay time.Duration +} + +func New(bus drivers.I2C) *Device { + return &Device{ + bus: bus, + Address: DefaultAddress, + standardDelay: defaultDelay, + } +} + +// SoftReset triggers a soft-reset of seesaw and waits for it to be ready +func (d *Device) SoftReset() error { + err := d.WriteRegister(ModuleStatusBase, FunctionStatusSwrst, 0xFF) + if err != nil { + return errors.New("failed sending soft-reset command: " + err.Error()) + } + + return d.waitForReset() +} + +func (d *Device) waitForReset() error { + + //give the device a little bit of time to reset + time.Sleep(time.Second) + + var lastErr error + tries := 0 + for ; tries < 20; tries++ { + _, err := d.readHardwareID() + if err == nil { + return nil + } + lastErr = err + time.Sleep(20 * time.Millisecond) + } + return errors.New("failed to wait for device to start: " + lastErr.Error()) +} + +func (d *Device) readHardwareID() (byte, error) { + hwid, err := d.ReadRegister(ModuleStatusBase, FunctionStatusHwId) + if err != nil { + return 0, err + } + + if hwid == seesawHwIdCodeSAMD09 || hwid == seesawHwIdCodeTINY8x7 { + return hwid, nil + } + + return 0, errors.New("unknown hardware ID: " + strconv.FormatUint(uint64(hwid), 16)) +} + +// WriteRegister writes a single seesaw register +func (d *Device) WriteRegister(module ModuleBaseAddress, function FunctionAddress, value byte) error { + var buf [3]byte + buf[0] = byte(module) + buf[1] = byte(function) + buf[2] = value + return d.bus.Tx(d.Address, buf[:], nil) +} + +// ReadRegister reads a single register from seesaw +func (d *Device) ReadRegister(module ModuleBaseAddress, function FunctionAddress) (byte, error) { + var buf [1]byte + err := d.Read(module, function, buf[:], d.standardDelay) + if err != nil { + return 0, err + } + return buf[0], nil +} + +// Read reads a number of bytes from the device after sending the read command and waiting 'standardDelay'. The delays depend +// on the module and function and are documented in the seesaw datasheet +func (d *Device) Read(module ModuleBaseAddress, function FunctionAddress, buf []byte, delay time.Duration) error { + var cmd [2]byte + cmd[0] = byte(module) + cmd[1] = byte(function) + + err := d.bus.Tx(d.Address, cmd[:], nil) + if err != nil { + return err + } + + //This is needed for the client seesaw device to flush its RX buffer and process the command. + //See seesaw datasheet for timings for specific modules. + time.Sleep(delay) + + return d.bus.Tx(d.Address, nil, buf) +} + +// Write writes data into a given module and function +func (d *Device) Write(module ModuleBaseAddress, function FunctionAddress, buf []byte) error { + cmd := []byte{byte(module), byte(function)} + data := append(cmd, buf...) + return d.bus.Tx(d.Address, data, nil) +} diff --git a/seesaw/seesaw_test.go b/seesaw/seesaw_test.go new file mode 100644 index 0000000..e25a53b --- /dev/null +++ b/seesaw/seesaw_test.go @@ -0,0 +1,84 @@ +package seesaw + +import ( + "reflect" + "testing" + "time" +) + +func TestDevice_SoftReset_success(t *testing.T) { + + mocked := newMockDev(t, 0x49, + when([]byte{0x00, 0x7F, 0xFF}, nil, nil), + when([]byte{0x00, 0x01}, nil, nil), + when(nil, []byte{0x55}, nil), + ) + + sut := New(mocked) + + err := sut.SoftReset() + assertEquals(t, err, nil) +} + +func TestDevice_WriteRegister(t *testing.T) { + + write := byte(0x1F) + mocked := newMockDev(t, 0x49, + when([]byte{0x01, 0x04, write}, nil, nil), + ) + + sut := New(mocked) + + err := sut.WriteRegister(ModuleGpioBase, FunctionGpioBulk, write) + assertEquals(t, err, nil) +} + +func TestDevice_ReadRegister(t *testing.T) { + + read := byte(0x23) + mocked := newMockDev(t, 0x49, + when([]byte{0x0F, 0x10}, nil, nil), + when(nil, []byte{read}, nil), + ) + + sut := New(mocked) + + r, err := sut.ReadRegister(ModuleTouchBase, FunctionTouchChannelOffset) + assertEquals(t, err, nil) + assertEquals(t, r, read) +} + +func TestDevice_Read(t *testing.T) { + + expectedRead := []byte{0x01, 0x02, 0x03, 0x04, 0x05} + mocked := newMockDev(t, 0x49, + when([]byte{0x0F, 0x10}, nil, nil), + when(nil, expectedRead, nil), + ) + + sut := New(mocked) + + var buf [5]byte + err := sut.Read(ModuleTouchBase, FunctionTouchChannelOffset, buf[:], time.Nanosecond) + assertEquals(t, err, nil) + assertEquals(t, buf[:], expectedRead) +} + +func TestDevice_Write(t *testing.T) { + + expectedWrite := []byte{0x01, 0x02, 0x03, 0x04, 0x05} + mocked := newMockDev(t, 0x49, + when(append([]byte{0x0E, 0x04}, expectedWrite...), nil, nil), + ) + + sut := New(mocked) + + err := sut.Write(ModuleNeoPixelBase, FunctionNeopixelBuf, expectedWrite) + assertEquals(t, err, nil) +} + +func assertEquals[e any](t *testing.T, actual, expected e) { + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("actual %+v != %+v not equals expected", actual, expected) + } +}