Add support for ams AS560x on-axis magnetic rotary position sensors

This commit is contained in:
Neil Davis
2023-02-01 17:50:53 +00:00
committed by Ron Evans
parent b0a971573b
commit c59b5a909b
9 changed files with 933 additions and 0 deletions
+2
View File
@@ -251,6 +251,8 @@ endif
@md5sum ./build/test.uf2 @md5sum ./build/test.uf2
tinygo build -size short -o ./build/test.hex -target=nucleo-wl55jc ./examples/lora/lorawan/atcmd/ tinygo build -size short -o ./build/test.hex -target=nucleo-wl55jc ./examples/lora/lorawan/atcmd/
@md5sum ./build/test.hex @md5sum ./build/test.hex
tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/as560x/main.go
@md5sum ./build/test.uf2
# rwildcard is a recursive version of $(wildcard) # rwildcard is a recursive version of $(wildcard)
+1
View File
@@ -62,6 +62,7 @@ The following 90 devices are supported.
| [AMG88xx 8x8 Thermal camera sensor](https://cdn-learn.adafruit.com/assets/assets/000/043/261/original/Grid-EYE_SPECIFICATIONS%28Reference%29.pdf) | I2C | | [AMG88xx 8x8 Thermal camera sensor](https://cdn-learn.adafruit.com/assets/assets/000/043/261/original/Grid-EYE_SPECIFICATIONS%28Reference%29.pdf) | I2C |
| [APA102 RGB LED](https://cdn-shop.adafruit.com/product-files/2343/APA102C.pdf) | SPI | | [APA102 RGB LED](https://cdn-shop.adafruit.com/product-files/2343/APA102C.pdf) | SPI |
| [APDS9960 Digital proximity, ambient light, RGB and gesture sensor](https://cdn.sparkfun.com/assets/learn_tutorials/3/2/1/Avago-APDS-9960-datasheet.pdf) | I2C | | [APDS9960 Digital proximity, ambient light, RGB and gesture sensor](https://cdn.sparkfun.com/assets/learn_tutorials/3/2/1/Avago-APDS-9960-datasheet.pdf) | I2C |
| [AS5600](https://ams.com/documents/20143/36005/AS5600_DS000365_5-00.pdf) / [AS5601](https://ams.com/documents/20143/36005/AS5601_DS000395_3-00.pdf) [on-axis magnetic rotary position sensors](https://ams.com/angle-position-on-axis) | I2C |
| [AT24CX 2-wire serial EEPROM](https://www.openimpulse.com/blog/wp-content/uploads/wpsc/downloadables/24C32-Datasheet.pdf) | I2C | | [AT24CX 2-wire serial EEPROM](https://www.openimpulse.com/blog/wp-content/uploads/wpsc/downloadables/24C32-Datasheet.pdf) | I2C |
| [AXP192 single Cell Li-Battery and Power System Management](https://github.com/m5stack/M5-Schematic/blob/master/Core/AXP192%20Datasheet_v1.1_en_draft_2211.pdf) | I2C | | [AXP192 single Cell Li-Battery and Power System Management](https://github.com/m5stack/M5-Schematic/blob/master/Core/AXP192%20Datasheet_v1.1_en_draft_2211.pdf) | I2C |
| [BBC micro:bit LED matrix](https://github.com/bbcmicrobit/hardware/blob/master/SCH_BBC-Microbit_V1.3B.pdf) | GPIO | | [BBC micro:bit LED matrix](https://github.com/bbcmicrobit/hardware/blob/master/SCH_BBC-Microbit_V1.3B.pdf) | GPIO |
+172
View File
@@ -0,0 +1,172 @@
// Product: https://ams.com/as5600
// Datasheet: https://ams.com/documents/20143/36005/AS5600_DS000365_5-00.pdf
package as560x // import tinygo.org/x/drivers/ams560x
import (
"time"
"tinygo.org/x/drivers"
)
// AS5600 includes MPOS & MANG in addition to ZPOS to set a 'narrower angle range'
// ZPOS enables setting the 'zero position' of the device to any RAW_ANGLE value.
// MPOS ('max position') & MANG 'max angle' enable a 'partial range' on the AS5600.
// The value in ANGLE is scaled & adjusted by the device according to ZPOS and MPOS/MANG.
// The entire 12-bit range is 'compressed' into the RAW_ANGLE range of ZPOS->MPOS
// (or ZPOS->ZPOS+MANG) thus enabling a higher resolution for a partial range.
// if ZPOS > MPOS (or ZPOS + MANG > 4095) i.e. the incremental range 'crosses zero'
// then the device will automatically compensate for the correct range.
// For RAW_ANGLE values outside of the partial range, ANGLE will be 'capped' at either
// 0 or 4095, depending on 'which end of the partial range is closer.'
// AS5600Device represents an ams AS5600 device driver accessed over I2C
type AS5600Device struct {
// promote BaseDevice
BaseDevice
}
// NewAS5600 creates a new AS5600Device given an I2C bus
func NewAS5600(bus drivers.I2C) AS5600Device {
// Create base device
baseDev := newBaseDevice(bus)
// Add AS5600 specific registers
baseDev.registers[MPOS] = newI2CRegister(MPOS, 0, 0xfff, 2, reg_read|reg_write|reg_program)
baseDev.registers[MANG] = newI2CRegister(MANG, 0, 0xfff, 2, reg_read|reg_write|reg_program)
// Add AS5600 specific 'virtual registers'
conf, ok := baseDev.registers[CONF]
if ok {
baseDev.registers[PWMF] = newVirtualRegister(conf, 6, 0b11)
baseDev.registers[OUTS] = newVirtualRegister(conf, 4, 0b11)
}
// Return the device
return AS5600Device{baseDev}
}
// Configure sets up the AMS AS5600 sensor device with the given configuration.
func (d *AS5600Device) Configure(cfg Config) error {
// Call the BaseDevice method to do the actual Configure
d.BaseDevice.Configure(cfg)
// For AS5600 devices we need to calculate the maxAngle on startup from ZPOS/MPOS/MANG
// These could have been permanently BURN'ed (by writing BURN register with BURN_ANGLE/BURN_SETTING)
// or may have already been written in previous runs without a power cycle since.
mpos, err := d.ReadRegister(MPOS)
if nil != err {
return err
}
mang, err := d.ReadRegister(MANG)
if nil != err {
return err
}
// Read ZPOS for side effect of caching only so that next calculateEffectiveMaxAngle() can't fail
if _, err = d.ReadRegister(ZPOS); nil != err {
return err
}
if mpos != 0 {
// If MPOS is set, use MPOS regardless of MANG
err = d.calculateEffectiveMaxAngle(MPOS, mpos)
} else if mang != 0 {
// If MANG is set and MPOS == 0, use MANG
err = d.calculateEffectiveMaxAngle(MANG, mang)
} else {
// if neither is set, we have no narrow range
d.maxAngle = NATIVE_ANGLE_RANGE
}
return err
}
// calculateEffectiveMaxAngle calculates d.maxAngle after one of ZPOS/MPOS/MANG have been written
func (d *AS5600Device) calculateEffectiveMaxAngle(register uint8, value uint16) error {
var zpos, mpos uint16 = 0, 0
var err error = nil
switch register {
case MANG:
d.maxAngle = value // The easy case
return nil
case ZPOS:
zpos = value
mpos, err = d.ReadRegister(MPOS)
case MPOS:
mpos = value
zpos, err = d.ReadRegister(ZPOS)
default:
panic("calculateEffectiveMaxAngle() can only work from ZPOS, MPOS or MANG")
}
if nil != err {
return err
}
// MANG is effectively MPOS-ZPOS
mang := int(mpos) - int(zpos)
// correct for mpos < zpos
if mang < 0 {
mang += NATIVE_ANGLE_RANGE
}
d.maxAngle = uint16(mang)
return nil
}
// WriteRegister writes the given value for the given register to the AS560x device via I2C
func (d *AS5600Device) WriteRegister(address uint8, value uint16) error {
// Call the BaseDevice method to do the actual write
if err := d.BaseDevice.WriteRegister(address, value); err != nil {
return err
}
// When either ZPOS/MANG/MPOS are set we need to recalculate maxAngle
// We also may need to invalidate some cached values for the other two registers
recalc := false
switch address {
case ZPOS:
// Setting a new ZPOS invalidates MPOS but not MANG
d.registers[MPOS].invalidate()
recalc = true
case MPOS:
// Setting a new MPOS invalidates MANG but not ZPOS
d.registers[MANG].invalidate()
recalc = true
case MANG:
// Setting a new MANG invalidates MPOS but not ZPOS
d.registers[MPOS].invalidate()
recalc = true
}
if recalc {
// Datasheet tells us to wait at least 1ms before reading back
time.Sleep(time.Millisecond * 10) // conservative wait
return d.calculateEffectiveMaxAngle(address, value)
}
return nil
}
// GetMaxPosition returns the 'max position' (MPOS) in different units
func (d *AS5600Device) GetMaxPosition(units AngleUnit) (uint16, float32, error) {
mpos, err := d.ReadRegister(MPOS)
if nil != err {
return 0, 0.0, err
}
// Convert to requested units
i, f := convertFromNativeAngle(mpos, NATIVE_ANGLE_RANGE, units)
return i, f, nil
}
// SetMaxPosition sets the 'max position' (MPOS) in different units
func (d *AS5600Device) SetMaxPosition(mpos float32, units AngleUnit) error {
return d.WriteRegister(MPOS, convertToNativeAngle(mpos, units))
}
// GetMaxAngle returns the 'max position' (MANG) in different units
func (d *AS5600Device) GetMaxAngle(units AngleUnit) (uint16, float32, error) {
mang, err := d.ReadRegister(MANG)
if nil != err {
return 0, 0.0, err
}
// Convert to requested units
i, f := convertFromNativeAngle(mang, NATIVE_ANGLE_RANGE, units)
return i, f, nil
}
// SetMaxAngle sets the 'max angle' (MANG) in different units
func (d *AS5600Device) SetMaxAngle(mang float32, units AngleUnit) error {
return d.WriteRegister(MANG, convertToNativeAngle(mang, units))
}
+22
View File
@@ -0,0 +1,22 @@
// Product: https://ams.com/as5601
// Datasheet: https://ams.com/documents/20143/36005/AS5601_DS000395_3-00.pdf
package as560x // import tinygo.org/x/drivers/ams560x
import "tinygo.org/x/drivers"
// AS5601Device represents an ams AS5601 device driver accessed over I2C
type AS5601Device struct {
BaseDevice // promote base device
}
// NewAS5601 creates a new AS5601Device given an I2C bus
func NewAS5601(bus drivers.I2C) AS5601Device {
// Create base device
baseDev := newBaseDevice(bus)
// Add AS5601 specific registers
baseDev.registers[ABN] = newI2CRegister(ABN, 0, 0b1111, 1, reg_read|reg_write|reg_program)
baseDev.registers[PUSHTHR] = newI2CRegister(PUSHTHR, 0, 0xff, 1, reg_read|reg_write|reg_program)
// Return the device
return AS5601Device{baseDev}
}
+198
View File
@@ -0,0 +1,198 @@
// Package as560x implements drivers for the ams AS5600/AS5601 on-axis magnetic rotary position sensors
//
// Product Pages:
// AS5600: https://ams.com/as5600
// AS5601: https://ams.com/as5601
//
// Datasheets:
// AS5600: https://ams.com/documents/20143/36005/AS5600_DS000365_5-00.pdf
// AS5601: https://ams.com/documents/20143/36005/AS5601_DS000395_3-00.pdf
//
package as560x // import tinygo.org/x/drivers/ams560x
import (
"errors"
"tinygo.org/x/drivers"
)
// Config holds the configuration for the AMS AS560x sensor devices.
type Config struct {
// Address is the I2C address of the AS560x device. If left zero this will default to 0x36
Address uint8
}
// MagnetStrength is an enum to indicate the magnetic field strength detected by the AS560x sensors.
type MagnetStrength int
const (
// MagnetTooWeak indicates that the magnet strength is too weak (AGC maximum gain overflow) - move it closer
MagnetTooWeak MagnetStrength = iota - 1
// MagnetOk indicates that the magnet strength is about right.
MagnetOk
// MagnetTooStrong indicates that the magnet strength is too strong (AGC minimum gain overflow) - move it further away
MagnetTooStrong
)
// AngleUnit is an enum to allow the use of different units when reading/writing angles from the AS560x sensors.
type AngleUnit int
const (
// ANGLE_NATIVE uses the device's native angle measurement. i.e. 12-bit integer, 0 <= angle <= 0xfff (4095)
ANGLE_NATIVE AngleUnit = iota
// ANGLE_DEGREES_INT measures angles in degrees using integer arithmetic for speed. i.e. 0 <= angle < 360
ANGLE_DEGREES_INT
// ANGLE_DEGREES_FLOAT measures angles in degrees using floating point (slower). i.e. 0.0 <= angle < 360.0
ANGLE_DEGREES_FLOAT
// ANGLE_RADIANS measures angles in radians using floating point (slower). i.e. 0.0 <= angle < 2 * PI
ANGLE_RADIANS
)
const (
// NATIVE_ANGLE_MAX is the maximum valid value for a native angle for a AS560x device
NATIVE_ANGLE_MAX = (1 << 12) - 1 + iota
// NATIVE_ANGLE_RANGE is the number of unique values for native angles for a AS560x device
NATIVE_ANGLE_RANGE
)
var (
errRegisterNotFound = errors.New("Register not found")
errMaxBurnAngle = errors.New("Max BURN_ANGLE limit reached")
)
// BaseDevice handles the common behaviour between AS5600 & AS5601 devices
type BaseDevice struct {
bus drivers.I2C
address uint8
registers map[uint8]*i2cRegister
maxAngle uint16
}
// newBaseDevice creates a new base device given an I2C bus.
func newBaseDevice(bus drivers.I2C) BaseDevice {
// Add all 'base' registers, common to both AS5600 & AS5601
conf := newI2CRegister(CONF, 0, 0x3fff, 2, reg_read|reg_write|reg_program)
status := newI2CRegister(STATUS, 0, 0xff, 1, reg_read)
regs := map[uint8]*i2cRegister{
ZPOS: newI2CRegister(ZPOS, 0, 0xfff, 2, reg_read|reg_write|reg_program),
CONF: conf,
RAW_ANGLE: newI2CRegister(RAW_ANGLE, 0, 0xfff, 2, reg_read),
ANGLE: newI2CRegister(ANGLE, 0, 0xfff, 2, reg_read),
STATUS: status,
AGC: newI2CRegister(AGC, 0, 0xff, 1, reg_read),
MAGNITUDE: newI2CRegister(MAGNITUDE, 0, 0xfff, 2, reg_read),
BURN: newI2CRegister(BURN, 0, 0xff, 1, reg_write),
// Add common 'virtual registers' These are bitfields within the common registers above
// A virtual register provides a convenient way to access the fields of a registers
// by handling all of the necessary bitfield shifting and masking operations
WD: newVirtualRegister(conf, 13, 0b1),
FTH: newVirtualRegister(conf, 10, 0b111),
SF: newVirtualRegister(conf, 8, 0b11),
HYST: newVirtualRegister(conf, 2, 0b11),
PM: newVirtualRegister(conf, 0, 0b11),
MD: newVirtualRegister(status, 5, 0b1),
ML: newVirtualRegister(status, 4, 0b1),
MH: newVirtualRegister(status, 3, 0b1),
}
return BaseDevice{bus, DefaultAddress, regs, NATIVE_ANGLE_RANGE}
}
// Configure sets up the AMS AS560x sensor device with the given configuration.
func (d *BaseDevice) Configure(cfg Config) {
if cfg.Address == 0 {
cfg.Address = DefaultAddress
}
d.address = cfg.Address
}
// ReadRegister reads the value for the given register from the AS560x device via I2C
func (d *BaseDevice) ReadRegister(address uint8) (uint16, error) {
reg, ok := d.registers[address]
if !ok {
return 0, errRegisterNotFound
}
return reg.read(d.bus, d.address)
}
// WriteRegister writes the given value for the given register to the AS560x device via I2C
func (d *BaseDevice) WriteRegister(address uint8, value uint16) error {
reg, ok := d.registers[address]
if !ok {
return errRegisterNotFound
}
return reg.write(d.bus, d.address, value)
}
// GetZeroPosition returns the 'zero position' (ZPOS) in various units
func (d *BaseDevice) GetZeroPosition(units AngleUnit) (uint16, float32, error) {
zpos, err := d.ReadRegister(ZPOS)
if nil != err {
return 0, 0.0, err
}
// Convert to requested units
i, f := convertFromNativeAngle(zpos, NATIVE_ANGLE_RANGE, units)
return i, f, nil
}
// SetZeroPosition sets the 'zero position' (ZPOS) in various units
func (d *BaseDevice) SetZeroPosition(zpos float32, units AngleUnit) error {
return d.WriteRegister(ZPOS, convertToNativeAngle(zpos, units))
}
// RawAngle reads the (unscaled & unadjusted) RAW_ANGLE register in various units
func (d *BaseDevice) RawAngle(units AngleUnit) (uint16, float32, error) {
angle, err := d.ReadRegister(RAW_ANGLE)
if nil != err {
return 0, 0.0, err
}
// Convert to requested units
i, f := convertFromNativeAngle(angle, NATIVE_ANGLE_RANGE, units)
return i, f, nil
}
// Angle reads the (scaled & adjusted) ANGLE register in various units
func (d *BaseDevice) Angle(units AngleUnit) (uint16, float32, error) {
// ZPOS enables setting the 'zero position' of the device to any RAW_ANGLE value
// ANGLE is RAW_ANGLE adjusted relative to ZPOS.
angle, err := d.ReadRegister(ANGLE)
if nil != err {
return 0, 0.0, err
}
// Convert to requested units
i, f := convertFromNativeAngle(angle, d.maxAngle, units)
return i, f, nil
}
// MagnetStatus reads the STATUS register and reports magnet position characteristics
func (d *BaseDevice) MagnetStatus() (detected bool, strength MagnetStrength, err error) {
status, err := d.ReadRegister(STATUS)
if nil != err {
return false, MagnetOk, err
}
detected = (status & STATUS_MD) != 0
strength = MagnetOk
if (status & STATUS_ML) != 0 {
strength = MagnetTooWeak
} else if (status & STATUS_MH) != 0 {
strength = MagnetTooStrong
}
return
}
// Burn is a convenience method to program the device permanently by writing to the BURN register (limited number of times use!)
func (d *BaseDevice) Burn(burnCmd BURN_CMD) error {
if BURN_ANGLE == burnCmd {
// BURN_ANGLE can only be executed up to 3 times.
// We can check this in advance by reading ZMCO before writing to the BURN register.
numBurns, err := d.ReadRegister(ZMCO)
if nil != err {
return err
}
if numBurns >= BURN_ANGLE_COUNT_MAX {
// We're outta BURNs :(
return errMaxBurnAngle
}
}
return d.WriteRegister(BURN, uint16(burnCmd))
}
+95
View File
@@ -0,0 +1,95 @@
package as560x // import tinygo.org/x/drivers/ams560x
import "math"
// convertFromNativeAngle converts and scales an angle from the device's native 12-bit range to the requested units
func convertFromNativeAngle(angle uint16, maxAngle uint16, units AngleUnit) (uint16, float32) {
// MANG == 0 & MANG == NATIVE_ANGLE_RANGE (1 << 12) mean the same thing: use full circle range
// but the latter makes the maths/code simpler
if 0 == maxAngle {
maxAngle = NATIVE_ANGLE_RANGE
}
switch units {
case ANGLE_NATIVE:
// For native angles, scaling has already been done by the device
return angle, float32(angle)
case ANGLE_DEGREES_INT:
// Convert to degrees using integer arithmetic. Less accuracy but faster
var deg int = 0
if NATIVE_ANGLE_RANGE == maxAngle {
// Simplify the conversion when using the full range
deg = int(angle) * 360 >> 12
} else {
// Using an integer degrees scale with a narrower native range is pointless since we don't
// benefit at all from the increase in native resolution, in fact we LOSE precision.
// Alas, we have to return something
// First get maxAngle on the degrees scale
degMang, _ := convertFromNativeAngle(maxAngle, NATIVE_ANGLE_RANGE, units)
// Now scale angle
deg = int(angle) * int(degMang) / NATIVE_ANGLE_RANGE
}
return uint16(deg), float32(deg)
case ANGLE_DEGREES_FLOAT:
// Convert to degrees using floating point. More accuracy at expense of speed
var degF float32 = 0.0
if NATIVE_ANGLE_RANGE == maxAngle {
// Simplify the conversion when using the full range
degF = float32(angle) * 360.0 / NATIVE_ANGLE_RANGE
} else {
// Scale to degrees using a narrower native range
// First get maxAngle on the degrees scale
_, degMangF := convertFromNativeAngle(maxAngle, NATIVE_ANGLE_RANGE, units)
// Now scale angle
degF = float32(angle) * degMangF / NATIVE_ANGLE_RANGE
}
return uint16(degF), degF
case ANGLE_RADIANS:
// Convert to radians. Can only be done using floating point.
var rad float32 = 0.0
if NATIVE_ANGLE_RANGE == maxAngle {
// Simplify the conversion when using the full range
rad = float32(angle) * 2 * math.Pi / NATIVE_ANGLE_RANGE
} else {
// Scale to radians using a narrower native range
// First get maxAngle on the radians scale
_, radMang := convertFromNativeAngle(maxAngle, NATIVE_ANGLE_RANGE, units)
// Now scale angle
rad = float32(angle) * radMang / NATIVE_ANGLE_RANGE
}
return uint16(rad), rad
default:
panic("Unknown angle measurement unit")
}
}
// convertToNativeAngle converts an angle from the requested units to the device's native 12-bit range.
func convertToNativeAngle(angle float32, units AngleUnit) uint16 {
var pos uint16 = 0
switch units {
case ANGLE_NATIVE:
pos = uint16(angle)
case ANGLE_DEGREES_INT:
fallthrough
case ANGLE_DEGREES_FLOAT:
// Convert from degrees
angle = float32(math.Mod(float64(angle), 360.0))
if angle < 0.0 {
angle += 360.0
}
pos = uint16(math.Round(float64(angle) * NATIVE_ANGLE_RANGE / 360.0))
case ANGLE_RADIANS:
// Convert from radians
const circRad = 2.0 * math.Pi
angle = float32(math.Mod(float64(angle), circRad))
if angle < 0.0 {
angle += circRad
}
pos = uint16(math.Round(float64(angle) * NATIVE_ANGLE_RANGE / circRad))
default:
panic("Unknown angle measurement unit")
}
if pos > NATIVE_ANGLE_MAX {
pos = NATIVE_ANGLE_MAX
}
return pos
}
+169
View File
@@ -0,0 +1,169 @@
package as560x // import tinygo.org/x/drivers/ams560x
import (
"encoding/binary"
"errors"
"tinygo.org/x/drivers"
)
// registerAttributes is a bitfield of attributes for a register
type registerAttributes uint8
const (
// reg_read indicates that the register is readable
reg_read registerAttributes = 1 << iota
// reg_write indicates that the register is writeable
reg_write
// reg_program indicates that the register can be permanently programmed ('BURNed')
reg_program
)
var (
errRegisterNotReadable = errors.New("Register is not readable")
errRegisterNotWriteable = errors.New("Register is not writeable")
)
// i2cRegister encapsulates the address, structure and read/write logic for a register on a AS560x device
type i2cRegister struct {
// host is the 'host register' for virtual registers. Physical/root registers have this set to self
host *i2cRegister
// address is the i2c address of the register. For 2-byte (word) addresses it's the low byte which holds the MSBs
address uint8
// shift is the number of bits the value is 'left shifted' into the register byte/word (0-15)
shift uint16
// mask is a bitwise mask applied to the register AFTER 'right shifting' to mask the register value
mask uint16
// num_bytes is the width of the register in bytes, 1 or 2.
num_bytes uint8
// attributes holds the register attributes. A bitfield of REG_xyz constants
attributes registerAttributes
// cached indicates whether we are holding a cached value of the register in value
cached bool
// value can be used as a 'cache' of the register's value for writeable registers.
value uint16
}
// newI2CRegister returns a pointer to a new i2cRegister with no cached value
func newI2CRegister(address uint8, shift uint16, mask uint16, num_bytes uint8, attributes registerAttributes) *i2cRegister {
reg := &i2cRegister{
address: address,
shift: shift,
mask: mask,
num_bytes: num_bytes,
attributes: attributes,
}
// root registers host themselves
reg.host = reg
return reg
}
// newVirtualRegister returns a pointer to a new i2cRegister with the given host register and shift/mask.
func newVirtualRegister(host *i2cRegister, shift uint16, mask uint16) *i2cRegister {
return &i2cRegister{
host: host,
address: host.address,
shift: shift,
mask: mask,
num_bytes: host.num_bytes,
attributes: host.attributes,
}
}
// invalidate invalidates any cached value for the register and forces an I2C read on the next read()
func (r *i2cRegister) invalidate() {
r.host.cached = false
r.host.value = 0
}
// readShiftAndMask is an internal method to read a value for the register over the given I2C bus from the device with the given address applying the given shift and mask
func (r *i2cRegister) readShiftAndMask(bus drivers.I2C, deviceAddress uint8, shift uint16, mask uint16) (uint16, error) {
if r.host.attributes&reg_read == 0 {
return 0, errRegisterNotReadable
}
// Only read over I2C if we don't have the host register value cached
var val uint16 = r.host.value
if !r.host.cached {
// To avoid an alloc we always use an array of 2 bytes
var buffer [2]byte
var buf []byte
if r.host.num_bytes < 2 {
buf = buffer[:1]
} else {
buf = buffer[:]
}
// Read the host register over I2C
err := bus.ReadRegister(deviceAddress, r.host.address, buf)
if nil != err {
return 0, err
}
// Unpack data from I2C
if r.host.num_bytes > 1 {
val = binary.BigEndian.Uint16(buf)
} else {
val = uint16(buf[0])
}
// cache this value if the host register is writeable. Note we cache the entire buffer without applying shift/mask
if r.host.attributes&reg_write != 0 {
r.host.value = val
r.host.cached = true
}
}
// Shift and mask the value before returning
val >>= shift
val &= mask
return val, nil
}
// read reads a value for the register over the given I2C bus from the device with the given address.
func (r *i2cRegister) read(bus drivers.I2C, deviceAddress uint8) (uint16, error) {
return r.readShiftAndMask(bus, deviceAddress, r.shift, r.mask)
}
// write writes a value for the register over the given I2C bus to the device with the given address.
func (r *i2cRegister) write(bus drivers.I2C, deviceAddress uint8, value uint16) error {
if r.host.attributes&reg_write == 0 {
return errRegisterNotWriteable
}
var newValue uint16 = 0
// Data sheet tells us to do a read first, modify only the desired bits and then write back
// since (quote:) 'Blank fields may contain factory settings'
// We will also need to do this anyway to support virtualRegister mappings on some registers
// (e.g. CONF/STATUS)
if (r.host.attributes & reg_read) > 0 { // not all registers are readable, e.g. BURN
// read the host register's entire host byte/word, regardless of shift & mask
readValue, error := r.readShiftAndMask(bus, deviceAddress, 0, 0xffff)
if error != nil {
return error
}
// Zero-out ONLY the relevant bits in newValue we just read
readValue &= (0xffff ^ (r.mask << r.shift))
newValue = readValue
}
// Mask the new value and shift it into place
value &= r.mask
value <<= r.shift
// OR the masked & shifted value back into newValue to be written
newValue |= value
// Pack newValue into a byte buffer to write. To avoid an alloc we always use an array of 2 bytes
var buffer [2]byte
var buf []byte
if r.host.num_bytes < 2 {
buf = buffer[:1]
buf[0] = uint8(newValue & 0xff)
} else {
buf = buffer[:]
binary.BigEndian.PutUint16(buf, newValue)
}
// Write the register from the buffer over I2C
err := bus.WriteRegister(deviceAddress, r.host.address, buf)
// after successful I2C write, cache this value if the host register (if also readable)
// Note we cache the entire buffer without applying shift/mask
if nil == err && r.host.attributes&reg_read != 0 {
r.host.value = newValue
r.host.cached = true
}
return err
}
+208
View File
@@ -0,0 +1,208 @@
package as560x // import tinygo.org/x/drivers/ams560x
// DefaultAddress is the default I2C address of the AMS AS560x sensors (0x36).
const DefaultAddress uint8 = 0x36
// AS560x common device registers
const (
// ZMCO contains the number of times a BURN_ANGLE command has been executed (max 3 burns)
ZMCO = 0x00
// ZPOS is the zero (start) position in RAW_ANGLE terms.
ZPOS = 0x01
// CONF supports custom config. Raw 14-bit register. See datasheet for mapping or use 'virtual registers' below.
CONF = 0x07
// STATUS indicates magnet position. Encapsulates MD, ML & MH. See also 'virtual registers' below.
STATUS = 0x0b
// RAW_ANGLE is the raw unscaled & unadjusted angle (12 bit: 0-4095/0xfff)
RAW_ANGLE = 0x0c
// ANGLE is RAW_ANGLE scaled & adjusted according to ZPOS (and MPOS/MANG on AS5600). (12 bit: 0-4095/0xfff)
ANGLE = 0x0e
// AGC is the Automatic Gain Control based on temp, airgap etc. 0-255 @ 5V, 0-128 @ 3.3V.
AGC = 0x1a
// MAGNITUDE indicates the magnitude value of the internal CORDIC output. See datasheet for more info.
MAGNITUDE = 0x1b
// BURN performs permanent programming of some registers. See BURN_XYZ cmd constants below for commands.
BURN = 0xff
)
// AS5600 specific registers
const (
// MPOS is the maximum position in RAW_ANGLE terms. With ZPOS, defines a 'narrower angle' for higher resolution.
MPOS = 0x03
// MANG is the maximum angle. With ZPOS, defines a 'narrower angle' for higher resolution.
MANG = 0x05
)
// AS5601 specific registers
const (
// ABN. See datasheet for mapping
ABN = 0x09
// PUSHTHR. Configures push-button function. See datasheet and AGC
PUSHTHR = 0x0a
)
// 'Virtual Registers' (VRs) are bitfields within the registers above.
// These are not real register addresses recognized by the chip,
// but they are recognized by the driver for convenience.
// virtualRegisterStartAddress defines the start of the virtual register address range.
const virtualRegisterStartAddress = 0xa0
const (
// VRs for CONF
// WD is a Virtual Register for the Watchdog timer. See WATCHDOG_TIMER consts.
WD = iota + virtualRegisterStartAddress
// FTH is a Virtual Register for the Fast Filter Threshold. See FAST_FILTER_THRESHOLD consts.
FTH
// SF is a Virtual Register for the Slow Filter. See SLOW_FILTER_RESPONSE consts.
SF
// PWMF is a Virtual Register for PWM Frequency (AS5600 ONLY). See PWM_FREQUENCY consts.
PWMF
// OUTS is a Virtual Register for the Output Stage (AS5600 ONLY). See OUTPUT_STAGE consts.
OUTS
// HYST is a Virtual Register for Hysteresis. See HYSTERESIS consts.
HYST
// PM is a Virtual Register for the Power Mode. See POWER_MODE consts.
PM
// VRs for STATUS (0 = unset, 1 = set)
// MD is a Virtual Register for the 'Magnet was detected' flag.
MD
// ML is a Virtual Register for the 'AGC maximum gain overflow' a.k.a 'magnet too weak' flag.
ML
// MH is a Virtual Register for the 'AGC minimum gain overflow' a.k.a 'magnet too strong' flag.
MH
)
// POWER_MODE values for the PM component of CONF (and the PM VR)
const (
// PM_NOM is the normal 'always on' power mode. No polling, max 6.5mA current
PM_NOM = iota
// PM_LPM1 is Low Power Mode 1. 5ms polling, max 3.4mA current
PM_LPM1
// PM_LPM2 is Low Power Mode 2. 20ms polling, max 1.8mA current
PM_LPM2
// PM_LPM3 is Low Power Mode 3. 100ms polling, max 1.5mA current
PM_LPM3
)
// HYSTERESIS values for the HYST component of CONF (and the HYST VR)
const (
// HYST_OFF disables any hysteresis of the output
HYST_OFF = iota
// HYST_1LSB enables output hysteresis using 1 LSB
HYST_1LSB
// HYST_2LSB enables output hysteresis using 2 LSBs
HYST_2LSB
// HYST_3LSB enables output hysteresis using 3 LSBs
HYST_3LSB
)
// OUTPUT_STAGE values for the OUTS component of CONF (and the OUTS VR - AS5600 ONLY)
const (
// OS_ANALOG_FULL_RANGE enables analog output with full range (0%-100% VDD)
OS_ANALOG_FULL_RANGE = iota
// OS_ANALOG_REDUCED_RANGE enables analog output with reduced range (10%-90% VDD)
OS_ANALOG_REDUCED_RANGE
// OS_DIGITAL_PWM enables digital PWM output. Frequency determined by PWMF
OS_DIGITAL_PWM
)
// PWM_FREQUENCY values for the PWMF component of CONF (and the PWMF VR - ASS5600 ONLY)
const (
// PWMF_115_HZ enables PWM at 115 Hz
PWMF_115_HZ = iota
// PWMF_230_HZ enables PWM at 230 Hz
PWMF_230_HZ
// PWMF_460_HZ enables PWM at 460 Hz
PWMF_460_HZ
// PWMF_920_HZ enables PWM at 920 Hz
PWMF_920_HZ
)
// SLOW_FILTER_RESPONSE values for the SF (slow filter) component of CONF (and the SF VR)
const (
// SF_16X enables a 16x Slow Filter step response
SF_16X = iota
// SF_8X enables a 8x Slow Filter step response
SF_8X
// SF_4X enables a 4x Slow Filter step response
SF_4X
// SF_2X enables a 2x Slow Filter step response
SF_2X
)
// FAST_FILTER_THRESHOLD values for the FTH (fast filter threshold) component of CONF (and the FTH VR)
const (
// FTH_NONE disables the fast filter (slow filter only)
FTH_NONE = iota
// FTH_6LSB enables a fast filter threshold with 6 LSBs
FTH_6LSB
// FTH_7LSB enables a fast filter threshold with 7 LSBs
FTH_7LSB
// FTH_9LSB enables a fast filter threshold with 9 LSBs
FTH_9LSB
// FTH_18LSB enables a fast filter threshold with 18 LSBs
FTH_18LSB
// FTH_21LSB enables a fast filter threshold with 21 LSBs
FTH_21LSB
// FTH_24LSB enables a fast filter threshold with 24 LSBs
FTH_24SB
// FTH_10LSB enables a fast filter threshold with 10 LSBs
FTH_10LSB
)
// WATCHDOG_TIMER values for the WD component of CONF (and the WD VR)
const (
// WD_OFF disables the Watchdog Timer
WD_OFF = iota
// WD_ON enables the Watchdog Timer (automatic entry into LPM3 low-power mode enabled)
WD_ON
)
// constants for the raw STATUS register bitfield value.
const (
// STATUS_MH is set in STATUS when the magnet field is too strong (AGC minimum gain overflow)
STATUS_MH = 1 << (iota + 3)
// STATUS_ML is set in STATUS when the magnet field is too weak (AGC maximum gain overflow)
STATUS_ML
// STATUS_MD is set n STATUS when the magnet is detected. Doesn't seem to work with some units.
STATUS_MD
)
// ABN_MAPPING values for the ABN register (AS5601 ONLY)
const (
// ABN_8 configures 8 output positions (61 Hz)
ABN_8 = iota
// ABN_16 configures 16 output positions (122 Hz)
ABN_16
// ABN_32 configures 32 output positions (244 Hz)
ABN_32
// ABN_64 configures 64 output positions (488 Hz)
ABN_64
// ABN_128 configures 128 output positions (976 Hz)
ABN_128
// ABN_256 configures 256 output positions (1.95 KHz)
ABN_256
// ABN_512 configures 512 output positions (3.9 KHz)
ABN_512
// ABN_1024 configures 1024 output positions (7.8 KHz)
ABN_1024
// ABN_2048 configures 2048 output positions (15.6 KHz)
ABN_2048
)
// BURN_CMD is a command to write to the BURN register.
type BURN_CMD uint16
const (
// BURN_ANGLE is the value to write to BURN to permanently program ZPOS & MPOS (Max 3 times!)
BURN_ANGLE BURN_CMD = 0x80
// BURN_SETTING is the value to write to BURN to permanently program MANG & CONF (ONCE ONLY!)
BURN_SETTING BURN_CMD = 0x40
)
// BURN_ANGLE_COUNT_MAX is a constant for the maximum number of times a BURN_ANGLE command can be executed. Compare this with ZMCO
const BURN_ANGLE_COUNT_MAX uint16 = 3
+66
View File
@@ -0,0 +1,66 @@
package main
import (
"machine"
"machine/usb/hid/mouse"
"math"
"time"
"tinygo.org/x/drivers/as560x"
)
func main() {
// Let's use the AS5600 to make the world's most useless mouse with just a single X-axis & no buttons (!)
machine.I2C0.Configure(machine.I2CConfig{
Frequency: machine.TWI_FREQ_400KHZ,
SDA: machine.GPIO4,
SCL: machine.GPIO5,
})
as5600 := as560x.NewAS5600(machine.I2C0)
as5600.Configure(as560x.Config{})
mouse := mouse.New()
lastAngle := -1
for {
time.Sleep(time.Millisecond * 10)
// Get the magnet status of the AS5600
magnetDetected, magnetStrength, err := as5600.MagnetStatus()
if err != nil {
continue
}
// Get the raw angle from the AS5600
angle, _, err := as5600.RawAngle(as560x.ANGLE_NATIVE)
if err != nil {
continue
}
str := ""
if !magnetDetected {
str += "NOT "
}
str += "detected. Strength is "
switch magnetStrength {
case as560x.MagnetTooWeak:
str += "too weak"
case as560x.MagnetTooStrong:
str += "too strong"
default:
str += "ok"
}
println("Raw angle:", angle, "Magnet was", str)
if lastAngle != -1 {
diff := int(angle) - lastAngle
// correct the zero crossover glitch
if diff < -0xc00 {
diff += 0xfff
} else if diff > 0xc00 {
diff -= 0xfff
}
// debounce the noise (could use the sensor's filters/hysteresis instead?)
if math.Abs(float64(diff)) > 2 {
// move the mouse x-axis in response to the AS5600
mouse.Move(diff, 0)
}
}
lastAngle = int(angle)
}
}