From c59b5a909bab1007b56e67e4664e4cf484488f4b Mon Sep 17 00:00:00 2001 From: Neil Davis Date: Wed, 1 Feb 2023 17:50:53 +0000 Subject: [PATCH] Add support for ams AS560x on-axis magnetic rotary position sensors --- Makefile | 2 + README.md | 1 + as560x/as5600.go | 172 +++++++++++++++++++++++++++++++++ as560x/as5601.go | 22 +++++ as560x/as560x.go | 198 ++++++++++++++++++++++++++++++++++++++ as560x/helpers.go | 95 ++++++++++++++++++ as560x/i2c_register.go | 169 ++++++++++++++++++++++++++++++++ as560x/registers.go | 208 ++++++++++++++++++++++++++++++++++++++++ examples/as560x/main.go | 66 +++++++++++++ 9 files changed, 933 insertions(+) create mode 100644 as560x/as5600.go create mode 100644 as560x/as5601.go create mode 100644 as560x/as560x.go create mode 100644 as560x/helpers.go create mode 100644 as560x/i2c_register.go create mode 100644 as560x/registers.go create mode 100644 examples/as560x/main.go diff --git a/Makefile b/Makefile index 9b874f5..df60b5d 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/README.md b/README.md index 697991c..571b252 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/as560x/as5600.go b/as560x/as5600.go new file mode 100644 index 0000000..fc572e3 --- /dev/null +++ b/as560x/as5600.go @@ -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)) +} diff --git a/as560x/as5601.go b/as560x/as5601.go new file mode 100644 index 0000000..d7431d6 --- /dev/null +++ b/as560x/as5601.go @@ -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} +} diff --git a/as560x/as560x.go b/as560x/as560x.go new file mode 100644 index 0000000..049ca80 --- /dev/null +++ b/as560x/as560x.go @@ -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)) +} diff --git a/as560x/helpers.go b/as560x/helpers.go new file mode 100644 index 0000000..2d4a28d --- /dev/null +++ b/as560x/helpers.go @@ -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 +} diff --git a/as560x/i2c_register.go b/as560x/i2c_register.go new file mode 100644 index 0000000..2a232ab --- /dev/null +++ b/as560x/i2c_register.go @@ -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 +} diff --git a/as560x/registers.go b/as560x/registers.go new file mode 100644 index 0000000..e37d607 --- /dev/null +++ b/as560x/registers.go @@ -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 diff --git a/examples/as560x/main.go b/examples/as560x/main.go new file mode 100644 index 0000000..c20e254 --- /dev/null +++ b/examples/as560x/main.go @@ -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) + } +}