Skip to content
Open
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
196 changes: 196 additions & 0 deletions internal/regmap/device8tx.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
// Package regmap provides transaction-based interfaces for reading and writing
// to device registers over I2C and SPI buses with pre-allocated buffers.
package regmap

import (
"errors"

"tinygo.org/x/drivers"
)

var (
// errNotInTx indicates an operation was attempted outside of an active transaction.
errNotInTx = errors.New("device not in Tx")

// errInTx indicates a transaction was started while another is still active.
errInTx = errors.New("device already in Tx")

// errShortWriteBuffer indicates the write buffer is too small for the requested operation.
errShortWriteBuffer = errors.New("device write buffer too short")

// errShortReadBuffer indicates the read buffer is too small for the requested operation.
errShortReadBuffer = errors.New("device read buffer too short")
)

// Device8Txer wraps a Device8 to provide buffered transaction support for
// I2C and SPI operations. It maintains pre-allocated buffers to avoid heap
// allocations during register access operations.
//
// Users must call SetBuffers to configure the write and read buffers before
// initiating transactions.
type Device8Txer struct {
Device8
writeBuf []byte // Pre-allocated buffer for write operations
readBuf []byte // Pre-allocated buffer for read operations
inTx bool // Tracks whether a transaction is currently active
}

// SetTxBuffers configures the write and read buffers for this device.
// These buffers are reused across transactions to avoid heap allocations.
//
// The writebuf should be large enough to hold the register address plus
// all data bytes to be written in a single transaction.
func (d *Device8Txer) SetTxBuffers(writebuf, readbuf []byte) {
d.readBuf = readbuf
d.writeBuf = writebuf
}

// Tx8 represents an active transaction for an 8-bit register device.
// It tracks the write buffer and current offset as data is added to the transaction.
//
// Use AddWriteByte or AddWriteData to add data to the transaction, then call
// DoTxI2C or DoTxSPI to execute the transaction over the bus.
type Tx8 struct {
dw *Device8Txer // Reference to the parent device
off int // Current offset in the write buffer
}

// Tx initiates a new transaction for writing to the specified register address.
//
// Parameters:
// - writeAddr: The 8-bit register address to write to
//
// Returns a Tx8 handle that can be used to add data and execute the transaction.
//
// Returns an error if:
// - A transaction is already active (errInTx)
// - The write buffer is too short (errShortWriteBuffer)
func (dw *Device8Txer) Tx(writeAddr uint8) (Tx8, error) {
if dw.inTx {
return Tx8{}, errInTx
} else if len(dw.writeBuf) < 1 {
return Tx8{}, errShortWriteBuffer
}
dw.writeBuf[0] = writeAddr
return Tx8{dw: dw, off: 1}, nil
}

// AddWriteData appends multiple bytes to the current transaction's write buffer.
//
// Parameters:
// - buf: Variable number of bytes to add to the transaction
//
// Returns an error if:
// - No transaction is active (errNotInTx)
// - The write buffer doesn't have enough space (errShortWriteBuffer)
func (tx *Tx8) AddWriteData(buf ...byte) error {
if !tx.dw.inTx {
return errNotInTx
}
avail := tx.dw.writeBuf[tx.off:]
if len(avail) < len(buf) {
return errShortWriteBuffer
}
n := copy(avail, buf)
tx.off += n
return nil
}

// AddWriteByte appends a single byte to the current transaction's write buffer.
//
// Parameters:
// - b: The byte to add to the transaction
//
// Returns an error if:
// - No transaction is active (errNotInTx)
// - The write buffer doesn't have enough space (errShortWriteBuffer)
func (tx *Tx8) AddWriteByte(b byte) error {
if !tx.dw.inTx {
return errNotInTx
}
avail := tx.dw.writeBuf[tx.off:]
if len(avail) < 1 {
return errShortWriteBuffer
}
avail[0] = b
tx.off++
return nil
}

// DoTxI2C executes the transaction over an I2C bus.
//
// This performs a combined write-read I2C transaction, first sending the
// register address and any data added to the transaction, then reading
// the specified number of bytes from the device.
//
// Parameters:
// - bus: The I2C bus to communicate over
// - deviceAddr: The I2C address of the target device
// - readLength: Number of bytes to read from the device
//
// Returns the read data as a slice of the internal read buffer, valid until
// the next transaction. The transaction is automatically freed after execution.
//
// Returns an error if:
// - No transaction is active (errNotInTx)
// - The read buffer is too short (errShortReadBuffer)
// - The I2C transaction fails
func (tx *Tx8) DoTxI2C(bus drivers.I2C, deviceAddr uint16, readLength int) ([]byte, error) {
if tx.off == 0 || !tx.dw.inTx {
return nil, errNotInTx
}
defer tx.freeTx()
if len(tx.dw.readBuf) < readLength {
return nil, errShortReadBuffer
}
rbuf := tx.dw.readBuf[:readLength]
err := bus.Tx(deviceAddr, tx.dw.writeBuf[:tx.off], rbuf)
if err != nil {
return nil, err
}
return rbuf, err
}

// DoTxSPI executes the transaction over an SPI bus.
//
// This performs a full-duplex SPI transaction, simultaneously writing the
// register address and data while reading the same number of bytes from the device.
//
// If no read buffer was configured (readBuf is nil), this performs a write-only
// transaction and returns nil without error.
//
// Parameters:
// - bus: The SPI bus to communicate over
//
// Returns the read data as a slice of the internal read buffer (same length as
// the write data), valid until the next transaction. The transaction is
// automatically freed after execution.
//
// Returns an error if:
// - No transaction is active (errNotInTx)
// - The read buffer is too short (errShortReadBuffer)
// - The SPI transaction fails
func (tx *Tx8) DoTxSPI(bus drivers.SPI) (readBuf []byte, err error) {
if tx.off == 0 || !tx.dw.inTx {
return nil, errNotInTx
}
defer tx.freeTx()
if tx.dw.readBuf == nil {
err = bus.Tx(tx.dw.writeBuf[:tx.off], nil) // Special case, only use write buffer functionality.
return nil, err
} else if len(readBuf) < tx.off {
return nil, errShortReadBuffer
}
rbuf := tx.dw.readBuf[:tx.off]
err = bus.Tx(tx.dw.writeBuf[:tx.off], rbuf)
if err != nil {
return nil, err
}
return rbuf, err
}

// freeTx marks the transaction as complete, allowing a new transaction to be started.
// This is called internally by DoTxI2C and DoTxSPI after the transaction completes.
func (tx *Tx8) freeTx() {
tx.dw.inTx = false
}
34 changes: 34 additions & 0 deletions internal/regmap/regmap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package regmap

import (
"fmt"

"tinygo.org/x/drivers"
)

func ExampleDevice8Txer() {
// Initialization.
var dtx Device8Txer
dtx.SetTxBuffers(make([]byte, 256), make([]byte, 256))

// Usage.
const (
defaultAddr = 65
REG_WRITE = 0x1f
IOCTL_CALL = 0xc0
)
tx, err := dtx.Tx(REG_WRITE)
if err != nil {
panic(err)
}
err = tx.AddWriteData(IOCTL_CALL, 0x80, 0x80)
if err != nil {
panic(err)
}
var bus drivers.I2C
readData, err := tx.DoTxI2C(bus, defaultAddr, 20)
if err != nil {
panic(err)
}
fmt.Println(readData)
}