mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-08-31 03:49:02 +00:00
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
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
// Package bno08x provides a TinyGo driver for the Adafruit BNO08x 9-DOF IMU sensors.
|
||||
//
|
||||
// This driver implements the CEVA SH-2 protocol over the SHTP transport layer,
|
||||
// providing access to orientation, motion, and environmental sensors.
|
||||
//
|
||||
// Datasheet: https://www.ceva-ip.com/wp-content/uploads/BNO080_085-Datasheet.pdf
|
||||
package bno08x
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
// Buser is the interface that wraps I2C or SPI bus operations.
|
||||
type Buser interface {
|
||||
configure(address uint16, readChunk int) error
|
||||
read(target []byte) (int, uint32, error)
|
||||
write(data []byte) error
|
||||
softReset() error
|
||||
}
|
||||
|
||||
// Device represents a BNO08x sensor device.
|
||||
type Device struct {
|
||||
bus Buser
|
||||
resetPin pin.OutputFunc
|
||||
|
||||
hal *hal
|
||||
shtp *shtp
|
||||
sh2 *sh2Protocol
|
||||
|
||||
queue [8]SensorValue
|
||||
queueHead int
|
||||
queueTail int
|
||||
queueCount int
|
||||
|
||||
productIDs ProductIDs
|
||||
lastReset bool
|
||||
}
|
||||
|
||||
// Config holds configuration options for the device.
|
||||
type Config struct {
|
||||
// Address is the I2C address (used only for I2C bus).
|
||||
Address uint16
|
||||
|
||||
// ResetPin is the optional hardware reset pin.
|
||||
ResetPin pin.OutputFunc
|
||||
|
||||
// ReadChunk is the I2C read chunk size (used only for I2C bus).
|
||||
ReadChunk int
|
||||
|
||||
// StartupDelay is the delay after reset (default: 100ms).
|
||||
StartupDelay time.Duration
|
||||
}
|
||||
|
||||
// Configure initializes the sensor and prepares it for use.
|
||||
func (d *Device) Configure(cfg Config) error {
|
||||
// Configure bus-specific settings
|
||||
if err := d.bus.configure(cfg.Address, cfg.ReadChunk); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cfg.ResetPin != nil {
|
||||
d.resetPin = cfg.ResetPin
|
||||
}
|
||||
if cfg.StartupDelay <= 0 {
|
||||
cfg.StartupDelay = 100 * time.Millisecond
|
||||
}
|
||||
|
||||
d.hal = newHAL(d)
|
||||
d.shtp = newSHTP(d.hal)
|
||||
d.sh2 = newSH2Protocol(d)
|
||||
|
||||
d.queueHead = 0
|
||||
d.queueTail = 0
|
||||
d.queueCount = 0
|
||||
d.productIDs = ProductIDs{}
|
||||
d.lastReset = false
|
||||
|
||||
if err := d.hal.open(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now that handlers are registered, perform reset
|
||||
// Try hardware reset first if available
|
||||
if d.resetPin != nil {
|
||||
d.hardwareReset()
|
||||
time.Sleep(cfg.StartupDelay)
|
||||
} else {
|
||||
// No hardware reset pin - try soft reset via bus
|
||||
if err := d.bus.softReset(); err != nil {
|
||||
// If that fails, try soft reset via SHTP protocol
|
||||
_ = d.sh2.softReset()
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for reset notification by actively polling
|
||||
// The sensor should send reset complete message shortly after reset
|
||||
deadline := time.Now().Add(1000 * time.Millisecond)
|
||||
pollCount := 0
|
||||
for time.Now().Before(deadline) {
|
||||
pollCount++
|
||||
if err := d.service(); err != nil {
|
||||
// Ignore errors during initial polling - sensor might not be ready
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
if d.lastReset {
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
|
||||
if !d.lastReset {
|
||||
return errTimeout
|
||||
}
|
||||
|
||||
// NOTE: We intentionally skip the Initialize command (sh2_initialize)
|
||||
// Testing revealed that sending the Initialize command (0xF2 0x00 0x04 0x01...)
|
||||
// prevents the BNO08x from sending sensor reports on channel 3.
|
||||
// The sensor works correctly without this command after a soft reset.
|
||||
// The Arduino library likely works because it does a hardware reset which
|
||||
// may put the sensor in a different state, or their initialization sequence
|
||||
// differs in a way that doesn't trigger this issue.
|
||||
|
||||
// Request product IDs
|
||||
if err := d.sh2.requestProductIDs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for product IDs with polling delay
|
||||
deadline = time.Now().Add(500 * time.Millisecond)
|
||||
for time.Now().Before(deadline) {
|
||||
if err := d.service(); err != nil {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
if d.productIDs.NumEntries > 0 {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
if d.productIDs.NumEntries == 0 {
|
||||
return errTimeout
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnableReport enables a specific sensor report at the given interval.
|
||||
func (d *Device) EnableReport(id SensorID, intervalUs uint32) error {
|
||||
err := d.sh2.enableReport(id, intervalUs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Poll a few times to let the sensor process the command
|
||||
// and potentially send acknowledgment
|
||||
for i := 0; i < 10; i++ {
|
||||
_ = d.service()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSensorConfig retrieves the current configuration for a sensor.
|
||||
func (d *Device) GetSensorConfig(id SensorID) (SensorConfig, error) {
|
||||
return d.sh2.getSensorConfig(id)
|
||||
}
|
||||
|
||||
// SetSensorConfig sets the configuration for a sensor.
|
||||
func (d *Device) SetSensorConfig(id SensorID, config SensorConfig) error {
|
||||
return d.sh2.setSensorConfig(id, config)
|
||||
}
|
||||
|
||||
// WasReset returns true if the sensor signaled a reset since the last call.
|
||||
func (d *Device) WasReset() bool {
|
||||
if d.lastReset {
|
||||
d.lastReset = false
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetSensorEvent retrieves the next available sensor event if present.
|
||||
func (d *Device) GetSensorEvent() (SensorValue, bool) {
|
||||
if d.queueCount == 0 {
|
||||
if err := d.service(); err != nil {
|
||||
return SensorValue{}, false
|
||||
}
|
||||
if d.queueCount == 0 {
|
||||
return SensorValue{}, false
|
||||
}
|
||||
}
|
||||
|
||||
value := d.queue[d.queueHead]
|
||||
d.queueHead = (d.queueHead + 1) % len(d.queue)
|
||||
d.queueCount--
|
||||
|
||||
return value, true
|
||||
}
|
||||
|
||||
// ProductIDs returns the cached product identification information.
|
||||
func (d *Device) ProductIDs() ProductIDs {
|
||||
return d.productIDs
|
||||
}
|
||||
|
||||
// Service processes pending sensor data.
|
||||
// This is called automatically by GetSensorEvent but can be called manually
|
||||
// for more control over timing.
|
||||
func (d *Device) Service() error {
|
||||
return d.service()
|
||||
}
|
||||
|
||||
func (d *Device) enqueue(value SensorValue) {
|
||||
next := (d.queueTail + 1) % len(d.queue)
|
||||
if d.queueCount == len(d.queue) {
|
||||
// Queue full, drop oldest
|
||||
d.queueHead = (d.queueHead + 1) % len(d.queue)
|
||||
d.queueCount--
|
||||
}
|
||||
d.queue[d.queueTail] = value
|
||||
d.queueTail = next
|
||||
d.queueCount++
|
||||
}
|
||||
|
||||
func (d *Device) service() error {
|
||||
if d.shtp == nil {
|
||||
return nil
|
||||
}
|
||||
for {
|
||||
processed, err := d.shtp.poll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !processed {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) hardwareReset() {
|
||||
if d.resetPin == nil {
|
||||
return
|
||||
}
|
||||
d.resetPin.High()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
d.resetPin.Low()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
d.resetPin.High()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
Reference in New Issue
Block a user