You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
devices/as7262/as7262.go

566 lines
15 KiB
Go

// Copyright 2018 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package as7262
import (
"context"
"encoding/binary"
"errors"
"fmt"
"math"
"sync"
"time"
"periph.io/x/conn/v3"
"periph.io/x/conn/v3/gpio"
"periph.io/x/conn/v3/i2c"
"periph.io/x/conn/v3/physic"
)
// Opts holds the configuration options.
type Opts struct {
InterruptPin gpio.PinIn
Gain Gain
}
// DefaultOpts are the recommended default options.
var DefaultOpts = Opts{
InterruptPin: nil,
Gain: G1x,
}
// New opens a handle to an AS7262 sensor.
func New(bus i2c.Bus, opts *Opts) (*Dev, error) {
// The nil or zero values for gain and interrupt are valid
c := make(chan struct{})
close(c)
return &Dev{
c: &i2c.Dev{Bus: bus, Addr: 0x49},
gain: opts.Gain,
interrupt: opts.InterruptPin,
cancel: func() {},
done: c,
}, nil
}
var sensorTimeout = 200 * time.Millisecond
// waitForSensor is overridden in tests.
var waitForSensor = time.After
// Dev is a handle to the as7262 sensor.
type Dev struct {
c conn.Conn
interrupt gpio.PinIn
// Mutable
mu sync.Mutex
gain Gain
// cancelMu guards cancel and done.
cancelMu sync.Mutex
cancel context.CancelFunc
done chan struct{}
}
// Spectrum is the reading from the sensor including the actual sensor state for
// the readings.
type Spectrum struct {
Bands []Band
SensorTemperature physic.Temperature
Gain Gain
LedDrive physic.ElectricCurrent
Integration time.Duration
}
func (s Spectrum) String() string {
str := fmt.Sprintf("Spectrum: Gain:%s, Led Drive %s, Sense Time: %s", s.Gain, s.LedDrive, s.Integration)
for _, band := range s.Bands {
str += "\n" + band.String()
}
return str
}
// Band has two types of measurement of relative spectral flux density.
//
// Value
//
// Value are the calibrated readings. The accuracy of the channel counts/μW/cm2
// is ±12%.
//
// Counts
//
// Counts are the raw readings, there are approximately 45 counts/μW/cm2 with a
// gain of 16 (Gx16).
//
// Wavelength
//
// Wavelength is the nominal center of a band, with a ±40nm bandwidth around the
// center. Wavelengths for the as7262 are: 450nm, 500nm, 550nm, 570nm, 600nm and
// 650nm.
type Band struct {
Wavelength physic.Distance
Value float64
Counts uint16
Name string
}
func (b Band) String() string {
return fmt.Sprintf("%s Band(%s) %7.1f counts", b.Name, b.Wavelength, b.Value)
}
// Sense preforms a reading of relative spectral radiance of all the sensor
// bands.
//
// Led Drive Current
//
// The AS7262 provides a current-limited integrated led drive circuit. Valid
// limits for the drive current are 0mA, 12.5mA, 25mA, 50mA and 100mA. If non
// valid values are given the next lowest valid value is used.
//
// Resolution
//
// For best resolution it is recommended that for a specific led drive
// current that the senseTime or gain is increased until at least one of the
// bands returns a count above 10000. The maximum senseTime time is 714ms
// senseTime will be quantised into intervals of of 2.8ms. Actual time taken to
// make a reading is twice the senseTime.
func (d *Dev) Sense(ledDrive physic.ElectricCurrent, senseTime time.Duration) (Spectrum, error) {
d.mu.Lock()
it, integration := calcSenseTime(senseTime)
ctx, cancel := context.WithCancel(context.Background())
d.cancelMu.Lock()
d.cancel = cancel
d.done = make(chan struct{})
d.cancelMu.Unlock()
defer func() {
close(d.done)
d.cancel()
d.mu.Unlock()
}()
if err := d.writeVirtualRegister(ctx, integrationReg, it); err != nil {
return Spectrum{}, err
}
led, drive := calcLed(ledDrive)
if err := d.writeVirtualRegister(ctx, ledControlReg, led); err != nil {
return Spectrum{}, err
}
if err := d.writeVirtualRegister(ctx, controlReg, allOneShot|uint8(d.gain)); err != nil {
return Spectrum{}, err
}
if d.interrupt != nil {
isEdge := make(chan bool)
go func() {
// TODO(NeuralSpaz): Test on hardware.
isEdge <- d.interrupt.WaitForEdge(integration*2 + sensorTimeout)
}()
select {
case edge := <-isEdge:
if !edge {
return Spectrum{}, errPinTimeout
}
case <-ctx.Done():
return Spectrum{}, errHalted
}
} else {
select {
// WaitForSensor is time.After().
case <-waitForSensor(integration * 2):
if err := d.pollDataReady(ctx); err != nil {
return Spectrum{}, err
}
case <-ctx.Done():
return Spectrum{}, errHalted
}
}
if err := d.writeVirtualRegister(ctx, ledControlReg, 0x00); err != nil {
return Spectrum{}, err
}
raw := make([]byte, 12)
if err := d.readVirtualRegister(ctx, rawBase, raw); err != nil {
return Spectrum{}, err
}
cal := make([]byte, 24)
if err := d.readVirtualRegister(ctx, calBase, cal); err != nil {
return Spectrum{}, err
}
v := binary.BigEndian.Uint16(raw[0:2])
b := binary.BigEndian.Uint16(raw[2:4])
g := binary.BigEndian.Uint16(raw[4:6])
y := binary.BigEndian.Uint16(raw[6:8])
o := binary.BigEndian.Uint16(raw[8:10])
r := binary.BigEndian.Uint16(raw[10:12])
vcal := float64(math.Float32frombits(binary.BigEndian.Uint32(cal[0:4])))
bcal := float64(math.Float32frombits(binary.BigEndian.Uint32(cal[4:8])))
gcal := float64(math.Float32frombits(binary.BigEndian.Uint32(cal[8:12])))
ycal := float64(math.Float32frombits(binary.BigEndian.Uint32(cal[12:16])))
ocal := float64(math.Float32frombits(binary.BigEndian.Uint32(cal[16:20])))
rcal := float64(math.Float32frombits(binary.BigEndian.Uint32(cal[20:24])))
traw := make([]byte, 1)
if err := d.readVirtualRegister(ctx, deviceTemperatureReg, traw); err != nil {
return Spectrum{}, err
}
temperature := physic.Temperature(int8(traw[0]))*physic.Kelvin + physic.ZeroCelsius
return Spectrum{
Bands: []Band{
{Wavelength: 450 * physic.NanoMetre, Counts: v, Value: vcal, Name: "V"},
{Wavelength: 500 * physic.NanoMetre, Counts: b, Value: bcal, Name: "B"},
{Wavelength: 550 * physic.NanoMetre, Counts: g, Value: gcal, Name: "G"},
{Wavelength: 570 * physic.NanoMetre, Counts: y, Value: ycal, Name: "Y"},
{Wavelength: 600 * physic.NanoMetre, Counts: o, Value: ocal, Name: "O"},
{Wavelength: 650 * physic.NanoMetre, Counts: r, Value: rcal, Name: "R"},
},
SensorTemperature: temperature,
Gain: d.gain,
LedDrive: drive,
Integration: integration,
}, nil
}
// Halt stops any pending operations. Repeated calls to Halt do nothing.
func (d *Dev) Halt() error {
d.cancelMu.Lock()
defer d.cancelMu.Unlock()
d.cancel()
// A receive can always proceed on a closed channel we can use that
// to signal that the running process has been canceled correctly.
<-d.done
return nil
}
func (d *Dev) String() string {
return "AMS AS7262 6 channel visible spectrum sensor"
}
// Gain is the sensor gain for all bands
type Gain int
const (
// G1x is gain of 1
G1x Gain = 0x00
// G4x is gain of 3.7
G4x Gain = 0x10
// G16x is a gain of 16
G16x Gain = 0x20
// G64x us a gain of 64
G64x Gain = 0x30
)
const (
_GainG1x = "1x"
_GainG4x = "3.7x"
_GainG16x = "16x"
_GainG64x = "64x"
)
func (g Gain) String() string {
switch {
case g == 0:
return _GainG1x
case g == 16:
return _GainG4x
case g == 32:
return _GainG16x
case g == 48:
return _GainG64x
default:
return "bad gain value"
}
}
// Gain sets the gain of the sensor. There are four levels of gain 1x, 3.7x, 16x,
// and 64x.
func (d *Dev) Gain(gain Gain) error {
if gain != G1x && gain != G4x && gain != G16x && gain != G64x {
return errGainValue
}
d.mu.Lock()
defer d.mu.Unlock()
if err := d.writeVirtualRegister(context.Background(), controlReg, uint8(gain)); err != nil {
return err
}
d.gain = gain
return nil
}
// AS7262 i2c protocol uses virtual registers. To write to a given register the
// MSB of the register must be set when writing the register to the write
// register, also status register must be checked for pending writes or data may
// be discarded.
func (d *Dev) writeVirtualRegister(ctx context.Context, register, data byte) error {
// Check for pending writes.
if err := d.pollStatus(ctx, writing); err != nil {
return err
}
// Set virtual register that is being written to.
if err := d.c.Tx([]byte{writeReg, register | 0x80}, nil); err != nil {
return &IOError{"setting virtual register", err}
}
// Check for pending writes again.
if err := d.pollStatus(ctx, writing); err != nil {
return err
}
// Write data to register that is being written to.
if err := d.c.Tx([]byte{writeReg, data}, nil); err != nil {
return &IOError{"writing virtual register", err}
}
return nil
}
// AS7262 protocol uses virtual registers. To read a virtual register the
// pointer to the virtual register must be written to the write register. Status
// register must be checked for any pending reads or data may be invalid, then
// data maybe read from the read register.
func (d *Dev) readVirtualRegister(ctx context.Context, register byte, data []byte) error {
rx := make([]byte, 1)
for i := 0; i < len(data); i++ {
// Check for pending reads.
if err := d.pollStatus(ctx, clearBuffer); err != nil {
return err
}
// Set virtual register that is being read from plus offset.
if err := d.c.Tx([]byte{writeReg, register + byte(i)}, nil); err != nil {
return &IOError{"setting virtual register", err}
}
// Check if read buffer is ready.
if err := d.pollStatus(ctx, reading); err != nil {
return err
}
// Read byte from register that is being read from into our buffer with
// offset.
if err := d.c.Tx([]byte{readReg}, rx); err != nil {
return &IOError{"reading virtual register", err}
}
data[i] = rx[0]
}
return nil
}
// Polls the data ready bit in the control register(virtual)
func (d *Dev) pollDataReady(ctx context.Context) error {
timeout := time.NewTimer(sensorTimeout)
defer timeout.Stop()
for {
if err := d.pollStatus(ctx, clearBuffer); err != nil {
return err
}
// Set virtual register that is being read from plus offset.
if err := d.c.Tx([]byte{writeReg, controlReg}, nil); err != nil {
return &IOError{"setting virtual register", err}
}
// Check if read buffer is ready.
if err := d.pollStatus(ctx, reading); err != nil {
return err
}
// Read byte from register that is being read from into our buffer with
// offset.
data := make([]byte, 1)
if err := d.c.Tx([]byte{readReg}, data); err != nil {
return &IOError{"reading virtual register", err}
}
if data[0]&0x02 > 0 {
return nil
}
select {
case <-time.After(5 * time.Millisecond):
// Polling interval.
case <-timeout.C:
// Return error if it takes too long.
return errStatusDeadline
case <-ctx.Done():
return errHalted
}
}
}
type direction byte
const (
// Reading is a bit mask for the status register.
reading direction = 1
// Writing is a bit mask for the status register.
writing direction = 2
// ClearBuffer clears any data left in the buffer and then checks the reading
clearBuffer direction = 3
)
// The as7262 registers are implemented as virtual registers pollStatus
// provides a way to repeatedly check if there are any pending reads or writes
// in the relevant buffer before a transaction while with a timeout.
// Direction is used to set which buffer is being polled to be ready.
func (d *Dev) pollStatus(ctx context.Context, dir direction) error {
timeout := time.NewTimer(sensorTimeout)
defer timeout.Stop()
select {
case <-ctx.Done():
return errHalted
default:
}
status := make([]byte, 1)
for {
// Read status register.
err := d.c.Tx([]byte{statusReg}, status)
if err != nil {
return &IOError{"reading status register", err}
}
switch dir {
case reading:
// Bit 0: rx valid bit.
// 0 → No data is ready to be read in READ register.
// 1 → Data byte available in READ register.
if status[0]&byte(dir) == 1 {
return nil
}
case writing:
// Bit 1: tx valid bit.
// 0 → New data may be written to WRITE register.
// 1 → WRITE register occupied. Do NOT write.
if status[0]&byte(dir) == 0 {
return nil
}
case clearBuffer:
// If there is data left in the buffer read it.
if status[0]&byte(reading) == 1 {
discard := make([]byte, 1)
if err := d.c.Tx([]byte{readReg}, discard); err != nil {
return &IOError{"clearing buffer", err}
}
}
if status[0]&byte(reading) == 0 {
return nil
}
}
select {
case <-time.After(5 * time.Millisecond):
// Polling interval.
case <-timeout.C:
// Return error if it takes too long.
return errStatusDeadline
case <-ctx.Done():
return errHalted
}
}
}
const (
maxSenseTime time.Duration = 714 * time.Millisecond
minSenseTime = 2800 * time.Microsecond
)
// calculateIntergrationTime converts a time.Duration into a value between 0 and
// 256
func calcSenseTime(t time.Duration) (uint8, time.Duration) {
if t > maxSenseTime {
return 255, maxSenseTime
}
if t < minSenseTime {
return 1, minSenseTime
}
// Minimum step is 2.8ms
quantizedTime := t / minSenseTime
return uint8(quantizedTime), quantizedTime * minSenseTime
}
func calcLed(drive physic.ElectricCurrent) (uint8, physic.ElectricCurrent) {
switch {
case drive < 12500*physic.MicroAmpere:
return 0x00, 0
case drive >= 12500*physic.MicroAmpere && drive < 25*physic.MilliAmpere:
return 0x08, 12500 * physic.MicroAmpere
case drive >= 25*physic.MilliAmpere && drive < 50*physic.MilliAmpere:
return 0x18, 25 * physic.MilliAmpere
case drive >= 50*physic.MilliAmpere && drive < 100*physic.MilliAmpere:
return 0x28, 50 * physic.MilliAmpere
default:
return 0x38, 100 * physic.MilliAmpere
}
}
const (
// allOneShot gets data from both banks once, and set the data ready bit in
// the status control register when complete requires 2x the integration
// time.
allOneShot uint8 = 0x0c
)
// IOError is a I/O specific error.
type IOError struct {
Op string
Err error
}
func (e *IOError) Error() string {
if e.Err != nil {
return "ioerror while " + e.Op + ": " + e.Err.Error()
}
return "ioerror while " + e.Op
}
var (
errStatusDeadline = errors.New("deadline exceeded reading status register")
errPinTimeout = errors.New("timeout waiting for interrupt signal on pin")
errHalted = errors.New("received halt command")
errGainValue = errors.New("invalid gain value")
)
const (
statusReg = 0x00
writeReg = 0x01
readReg = 0x02
hardwareVersion = 0x00
firmwareVersion = 0x02
controlReg = 0x04
integrationReg = 0x05
deviceTemperatureReg = 0x06
ledControlReg = 0x07
// RawBase used as base for reading uint16 values, data must be sequentially.
rawBase = 0x08
rawVReg = 0x08
rawBReg = 0x0a
rawGReg = 0x0c
rawYReg = 0x0e
rawOReg = 0x10
rawRReg = 0x12
// CalBase used as base for reading float32 values, data must be sequentially.
calBase = 0x14
calibratedVReg = 0x14
calibratedBReg = 0x18
calibratedGReg = 0x1c
calibratedYReg = 0x20
calibratedOReg = 0x24
calibratedRReg = 0x28
)