Files
Mike Hughes 936a255df9 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
2025-12-10 19:07:04 +01:00

174 lines
4.2 KiB
Go

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
}