Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions examples/mpu6050/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,27 @@ import (
func main() {
machine.I2C0.Configure(machine.I2CConfig{})

accel := mpu6050.New(machine.I2C0)
accel.Configure()
mpuDevice := mpu6050.New(machine.I2C0, mpu6050.DefaultAddress)
Copy link
Copy Markdown
Member

@deadprogram deadprogram May 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other drivers, this has been solved by exporting Address and then just setting it before calling Configure. That way we do not have to break the existing API and all of the many projects that use this driver. I have several such projects myself 😺

mpuDevice := mpu6050.New(machine.I2C0)
mpuDevice.Address = 0x66 // overrides default

// now do everything else

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With Go modules, I'm not too worried about breakage. Yes, it will break once you do a go get -u tinygo.org/x/drivers, but before that it will keep working just fine (and if you're doing a go get -u tinygo.org/x/drivers, you might as well add the default address).

I'm fine either way.


err := mpuDevice.Configure(mpu6050.Config{
AccRange: mpu6050.ACCEL_RANGE_16,
GyroRange: mpu6050.GYRO_RANGE_2000,
})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...and here we have another breaking change, that I think is actually really useful.

if err != nil {
panic(err.Error())
}
for {
x, y, z := accel.ReadAcceleration()
println(x, y, z)
time.Sleep(time.Millisecond * 100)
err := mpuDevice.Update()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe Update should have a parameter with the sensor values it should update?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we should probably solve #345 before merging

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#345 has been merged, so this can be updated.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd leave this for last since I still don't have a good rebase methodology. As soon as everything else is reviewed and OK I'll try my hand at rebasing one more time

if err != nil {
println("error reading from mpu6050:", err.Error())
continue
}
print("acceleration: ")
println(mpuDevice.Acceleration())
print("angular velocity:")
println(mpuDevice.AngularVelocity())
print("temperature centigrade:")
println(mpuDevice.Temperature() / 1000)
Comment thread
soypat marked this conversation as resolved.
Outdated
}
}
281 changes: 209 additions & 72 deletions mpu6050/mpu6050.go
Original file line number Diff line number Diff line change
@@ -1,95 +1,232 @@
// Package mpu6050 provides a driver for the MPU6050 accelerometer and gyroscope
// made by InvenSense.
//
// Datasheets:
// https://store.invensense.com/datasheets/invensense/MPU-6050_DataSheet_V3%204.pdf
// https://www.invensense.com/wp-content/uploads/2015/02/MPU-6000-Register-Map1.pdf
package mpu6050 // import "tinygo.org/x/drivers/mpu6050"
Comment thread
soypat marked this conversation as resolved.
package mpu6050

import (
"encoding/binary"
"errors"

"tinygo.org/x/drivers"
)

import "tinygo.org/x/drivers"
const DefaultAddress = 0x68

// Device wraps an I2C connection to a MPU6050 device.
type Config struct {
// Use ACCEL_RANGE_2 through ACCEL_RANGE_16.
AccRange byte
Comment thread
soypat marked this conversation as resolved.
Outdated
// Use GYRO_RANGE_250 through GYRO_RANGE_2000
GyroRange byte
sampleRatio byte // TODO(soypat): expose these as configurable.
clkSel byte
}

// Device contains MPU board abstraction for usage
type Device struct {
bus drivers.I2C
Address uint16
conn drivers.I2C
aRange int32 //Gyroscope FSR acording to SetAccelRange input
gRange int32 //Gyroscope FSR acording to SetGyroRange input
Comment on lines +43 to +44
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FSR?

// RawData contains the accelerometer, gyroscope and temperature RawData read
// in the last call via the Update method.
RawData [14]byte
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't agree with exposing these. They are implementation details of the driver. Exposing these would make the mpu6050 a leaky abstraction.

address byte
}

// New creates a new MPU6050 connection. The I2C bus must already be
// configured.
//
// This function only creates the Device object, it does not touch the device.
func New(bus drivers.I2C) Device {
return Device{bus, Address}
// New instantiates and initializes a MPU6050 struct without writing/reading
// i2c bus. Typical I2C MPU6050 address is 0x68.
func New(bus drivers.I2C, addr uint16) *Device {
p := &Device{}
p.address = uint8(addr)
p.conn = bus
return p
}

// Connected returns whether a MPU6050 has been found.
// It does a "who am I" request and checks the response.
func (d Device) Connected() bool {
data := []byte{0}
d.bus.ReadRegister(uint8(d.Address), WHO_AM_I, data)
return data[0] == 0x68
Comment on lines -27 to -30
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you remove this method?

// Init configures the necessary registers for using the
// MPU6050. It sets the range of both the accelerometer
// and the gyroscope, the sample rate, the clock source
// and wakes up the peripheral.
func (p *Device) Configure(data Config) (err error) {
if err = p.Sleep(false); err != nil {
return errors.New("set sleep: " + err.Error())
}
if err = p.SetClockSource(data.clkSel); err != nil {
return errors.New("set clksrc: " + err.Error())
}
if err = p.SetSampleRate(data.sampleRatio); err != nil {
return errors.New("set sampleratio: " + err.Error())
}
if err = p.SetRangeGyro(data.GyroRange); err != nil {
return errors.New("set gyrorange: " + err.Error())
}
if err = p.SetRangeAccel(data.AccRange); err != nil {
return errors.New("set accelrange: " + err.Error())
}
return nil
Comment on lines +46 to +83
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Are you intentionally defaulting to very low GyroRange and AccRange?
  2. This creates a bunch of heap allocations in the error paths. I would suggest returning the error directly instead of wrapping it in errors.New.

}

// Configure sets up the device for communication.
func (d Device) Configure() error {
return d.SetClockSource(CLOCK_INTERNAL)
// Update fetches the latest data from the MPU6050
func (p *Device) Update() (err error) {
if err = p.read(_ACCEL_XOUT_H, p.RawData[:]); err != nil {
return err
}
return nil
}

// ReadAcceleration reads the current acceleration from the device and returns
// it in µg (micro-gravity). When one of the axes is pointing straight to Earth
// Acceleration returns last read acceleration in µg (micro-gravity).
// When one of the axes is pointing straight to Earth
// and the sensor is not moving the returned value will be around 1000000 or
// -1000000.
func (d Device) ReadAcceleration() (x int32, y int32, z int32) {
data := make([]byte, 6)
d.bus.ReadRegister(uint8(d.Address), ACCEL_XOUT_H, data)
// Now do two things:
// 1. merge the two values to a 16-bit number (and cast to a 32-bit integer)
// 2. scale the value to bring it in the -1000000..1000000 range.
// This is done with a trick. What we do here is essentially multiply by
// 1000000 and divide by 16384 to get the original scale, but to avoid
// overflow we do it at 1/64 of the value:
// 1000000 / 64 = 15625
// 16384 / 64 = 256
x = int32(int16((uint16(data[0])<<8)|uint16(data[1]))) * 15625 / 256
y = int32(int16((uint16(data[2])<<8)|uint16(data[3]))) * 15625 / 256
z = int32(int16((uint16(data[4])<<8)|uint16(data[5]))) * 15625 / 256
return
func (d *Device) Acceleration() (ax, ay, az int32) {
const accelOffset = 0
ax = int32(convertWord(d.RawData[accelOffset+0:])) * 15625 / 512 * d.aRange
ay = int32(convertWord(d.RawData[accelOffset+2:])) * 15625 / 512 * d.aRange
az = int32(convertWord(d.RawData[accelOffset+4:])) * 15625 / 512 * d.aRange
return ax, ay, az
}

// ReadRotation reads the current rotation from the device and returns it in
// µ°/s (micro-degrees/sec). This means that if you were to do a complete
// Rotations reads the current rotation from the device and returns it in
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AngularVelocity, not Rotation.

// µ°/rad (micro-radians/sec). This means that if you were to do a complete
// rotation along one axis and while doing so integrate all values over time,
// you would get a value close to 360000000.
func (d Device) ReadRotation() (x int32, y int32, z int32) {
data := make([]byte, 6)
d.bus.ReadRegister(uint8(d.Address), GYRO_XOUT_H, data)
// First the value is converted from a pair of bytes to a signed 16-bit
// value and then to a signed 32-bit value to avoid integer overflow.
// Then the value is scaled to µ°/s (micro-degrees per second).
// This is done in the following steps:
// 1. Multiply by 250 * 1000_000
// 2. Divide by 32768
// The following calculation (x * 15625 / 2048 * 1000) is essentially the
// same but avoids overflow. First both operations are divided by 16 leading
// to multiply by 15625000 and divide by 2048, and then part of the multiply
// is done after the divide instead of before.
x = int32(int16((uint16(data[0])<<8)|uint16(data[1]))) * 15625 / 2048 * 1000
y = int32(int16((uint16(data[2])<<8)|uint16(data[3]))) * 15625 / 2048 * 1000
z = int32(int16((uint16(data[4])<<8)|uint16(data[5]))) * 15625 / 2048 * 1000
return
// you would get a value close to 6.3 radians (360 degrees).
func (d *Device) AngularVelocity() (gx, gy, gz int32) {
const angvelOffset = 8
_ = d.RawData[angvelOffset+5] // This line fails to compile if RawData is too short.
gx = int32(convertWord(d.RawData[angvelOffset+0:])) * 4363 / 8192 * d.gRange
gy = int32(convertWord(d.RawData[angvelOffset+2:])) * 4363 / 8192 * d.gRange
gz = int32(convertWord(d.RawData[angvelOffset+4:])) * 4363 / 8192 * d.gRange
return gx, gy, gz
}

// SetClockSource allows the user to configure the clock source.
func (d Device) SetClockSource(source uint8) error {
return d.bus.WriteRegister(uint8(d.Address), PWR_MGMT_1, []uint8{source})
// Temperature returns the temperature of the device in milli-centigrade.
func (d *Device) Temperature() (Celsius int32) {
const tempOffset = 6
return 1506*int32(convertWord(d.RawData[tempOffset:]))/512 + 37*1000
}

func convertWord(buf []byte) int16 {
return int16(binary.BigEndian.Uint16(buf))
}

// SetSampleRate sets the sample rate for the FIFO,
// register ouput and DMP. The sample rate is determined
// by:
//
// SR = Gyroscope Output Rate / (1 + srDiv)
//
// The Gyroscope Output Rate is 8kHz when the DLPF is
// disabled and 1kHz otherwise. The maximum sample rate
// for the accelerometer is 1kHz, if a higher sample rate
// is chosen, the same accelerometer sample will be output.
func (p *Device) SetSampleRate(srDiv byte) (err error) {
// setSampleRate
var sr [1]byte
sr[0] = srDiv
if err = p.write8(_SMPRT_DIV, sr[0]); err != nil {
return err
}
return nil
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a new API.
Why not use time.Duration as an input? Or a sample rate in Hz, as an uint32? (Similar to machine.CPUFrequency() for example). The parameter that you're using here looks like a mpu6050-specific value.


// SetClockSource configures the source of the clock
// for the peripheral.
func (p *Device) SetClockSource(clkSel byte) (err error) {
// setClockSource
var pwrMgt [1]byte

if err = p.read(_PWR_MGMT_1, pwrMgt[:]); err != nil {
return err
}
pwrMgt[0] = (pwrMgt[0] & (^_CLK_SEL_MASK)) | clkSel // Escribo solo el campo de clk_sel
if err = p.write8(_PWR_MGMT_1, pwrMgt[0]); err != nil {
return err
}
return nil
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually useful? Can you give me an example where this would be used in practice?

If not, I suggest removing it to declutter the API.

(Also, it contains a Spanish comment?)


// SetRangeGyro configures the full scale range of the gyroscope.
// It has four possible values +- 250°/s, 500°/s, 1000°/s, 2000°/s.
// The function takes values of gyroRange from 0-3 where 0 means the
// lowest FSR (250°/s) and 3 is the highest FSR (2000°/s).
func (p *Device) SetRangeGyro(gyroRange byte) (err error) {
switch gyroRange {
case GYRO_RANGE_250:
p.gRange = 250
case GYRO_RANGE_500:
p.gRange = 500
case GYRO_RANGE_1000:
p.gRange = 1000
case GYRO_RANGE_2000:
p.gRange = 2000
default:
return errors.New("invalid gyroscope FSR input")
}
// setFullScaleGyroRange
var gConfig [1]byte

if err = p.read(_GYRO_CONFIG, gConfig[:]); err != nil {
return err
}
gConfig[0] = (gConfig[0] & (^_G_FS_SEL)) | (gyroRange << _G_FS_SHIFT) // Escribo solo el campo de FS_sel

if err = p.write8(_GYRO_CONFIG, gConfig[0]); err != nil {
return err
}
return nil
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make gyroRange a type? Like is done in the LIS3DH driver:

func (d *Device) SetRange(r Range) {

type Range uint8
const (
RANGE_16_G Range = 3 // +/- 16g
RANGE_8_G = 2 // +/- 8g
RANGE_4_G = 1 // +/- 4g
RANGE_2_G = 0 // +/- 2g (default value)
)

This makes it possible to easily change the type at a later time, if needed.


// SetRangeAccel configures the full scale range of the accelerometer.
// It has four possible values +- 2g, 4g, 8g, 16g.
// The function takes values of accRange from 0-3 where 0 means the
// lowest FSR (2g) and 3 is the highest FSR (16g)
func (p *Device) SetRangeAccel(accRange byte) (err error) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. Please don't use magic constants (0, 1, 2, 3) and instead instruct users to use named constants.

switch accRange {
case ACCEL_RANGE_2:
p.aRange = 2
case ACCEL_RANGE_4:
p.aRange = 4
case ACCEL_RANGE_8:
p.aRange = 8
case ACCEL_RANGE_16:
p.aRange = 16
default:
return errors.New("invalid accelerometer FSR input")
}

var aConfig [1]byte
if err = p.read(_ACCEL_CONFIG, aConfig[:]); err != nil {
return err
}
aConfig[0] = (aConfig[0] & (^_AFS_SEL)) | (accRange << _AFS_SHIFT)

if err = p.write8(_ACCEL_CONFIG, aConfig[0]); err != nil {
return err
}
return nil
}

// SetFullScaleGyroRange allows the user to configure the scale range for the gyroscope.
func (d Device) SetFullScaleGyroRange(rng uint8) error {
return d.bus.WriteRegister(uint8(d.Address), GYRO_CONFIG, []uint8{rng})
// Sleep sets the sleep bit on the power managment 1 field.
// When the recieved bool is true, it sets the bit to 1 thus putting
// the peripheral in sleep mode.
// When false is recieved the bit is set to 0 and the peripheral wakes up.
func (p *Device) Sleep(sleepEnabled bool) (err error) {
// setSleepBit
var pwrMgt [1]byte
if err = p.read(_PWR_MGMT_1, pwrMgt[:]); err != nil {
return err
}
if sleepEnabled {
pwrMgt[0] = (pwrMgt[0] & (^_SLEEP_MASK)) | (1 << _SLEEP_SHIFT) // Overwrite only Sleep
} else {
pwrMgt[0] = (pwrMgt[0] & (^_SLEEP_MASK))
}
if err = p.write8(_PWR_MGMT_1, pwrMgt[0]); err != nil {
return err
}
return nil
}

// SetFullScaleAccelRange allows the user to configure the scale range for the accelerometer.
func (d Device) SetFullScaleAccelRange(rng uint8) error {
return d.bus.WriteRegister(uint8(d.Address), ACCEL_CONFIG, []uint8{rng})
func DefaultConfig() Config {
return Config{
AccRange: ACCEL_RANGE_16,
GyroRange: GYRO_RANGE_2000,
sampleRatio: 0, // TODO add const values.
clkSel: 0,
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of DefaultConfig, have you considered making the zero value the default config?
That way, users can just do Configure(mpu6050.Config{GyroRange: ...}) and know that the acceleration range is also set to some reasonable default.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have pushed changes that make the zero value useful in latest commit!

Loading