mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-07-26 10:38:41 +00:00
Add support for ams AS560x on-axis magnetic rotary position sensors
This commit is contained in:
@@ -251,6 +251,8 @@ endif
|
||||
@md5sum ./build/test.uf2
|
||||
tinygo build -size short -o ./build/test.hex -target=nucleo-wl55jc ./examples/lora/lorawan/atcmd/
|
||||
@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)
|
||||
|
||||
@@ -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 |
|
||||
| [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 |
|
||||
| [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 |
|
||||
| [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 |
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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®_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®_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®_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®_read != 0 {
|
||||
r.host.value = newValue
|
||||
r.host.cached = true
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user