mirror of https://github.com/periph/devices
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.
566 lines
15 KiB
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
|
|
)
|