May 03, 2021

Wildflowers at Mori Point, Pacifica CA

Ocean. Hill. Beach. Fog. Drizzle. Wildflowers.
Perfect.

(Best viewed on a big screen).



























April 27, 2021

Versioned Encrypted Compressed backup


Versioned Encrypted Compressed backup.
  • Backup multiple versions locally or to the cloud or remote destinations using rclone.
  • De-duplicates chunks based on content checksums (sha512_256)
  • Optionally compresses (zlib)
  • Optionally password protect and encrypt backups with authenticated encryption (PBKDF2+NaCl)
  • MIT license.
  • Supported platforms: MacOS, Linux, Windows 10.
Yes, I wrote it. Written in Golang. Small. Few dependencies.

I have been using it for years.
If you find it useful, drop me a note.
Feedback welcomed.

Disclaimer: No warranty, use at your own risk.

November 01, 2020

Balancing Bot Update 2020

Since the last post three years ago, Micro:bit MakeCode has been updated.

The latest version is MakeCode v3 2020 Update. The balancing bot code was written using MakeCode v0. MakeCode v3 is not compatible with MakeCode v0.

The big incompatible change is that numbers in MakeCode v3 are now floating point while numbers in MakeCode v0 were 16 bit integers. MakeCode v3 also includes trigonometric functions in its library.

Here's the updated bot code that works with MakeCode v3. The Trig library should be removed from the custom.ts file. It is not needed anymore.

/**
 * Public domain. Use at your own risk!
 */
let TARGET_ANGLE = 155
let KE = 420
let KD = 8
let KI = 5
let motor_left_bias = -2;
let motor_right_bias = 2;
function control_loop() {
    motor_coast()
    let motor_out = 0;
    let vertical_count = 0;
    let motor_on = false
    let est_angle = read_accel_tilt_angle()
    let start_time = input.runningTime()
    let last_time = start_time
    let last_err = 0;
    let i_err = 0;
    while (true) {
        let current_time = input.runningTime()
        let delta_t = current_time - last_time
        last_time = current_time
        est_angle = updateAngle(est_angle, delta_t)
        if (motor_on && (est_angle > 3000 || est_angle < -3000)) {
            motor_coast()
            motor_on = false
        }
        let err = est_angle - TARGET_ANGLE
        if (motor_on) {
            let d_err = (err - last_err) * 1000 / delta_t
            last_err = err
            i_err = i_err + err * delta_t
            let u = err * KE + d_err * KD + i_err * KI
            motor_out = Math.clamp(-90, 90, u / 10000)
            motor_move(motor_out + motor_left_bias, motor_out + motor_right_bias)
        } else if (err <= 500 && err >= -500) {
            vertical_count = vertical_count + 1
            if (vertical_count > 100) {
                vertical_count = 0
                last_err = 0
                i_err = 0
                est_angle = read_accel_tilt_angle()
                motor_on = true
            }   
        } else {
            vertical_count = 0
        }        
        basic.pause(1)
    }
}

function read_gyro_angle_rate(): number {
    InvMPU.read_gyro()
    return -InvMPU.gyro_y
}

function read_accel_tilt_angle(): number {
    InvMPU.read_accel()
    return Math.round(Math.atan2(InvMPU.accel_z, -InvMPU.accel_x) * 18000 / Math.PI)
}

function updateAngle(old_angle: number, delta_t_ms: number): number {
    let accel_angle = read_accel_tilt_angle()
    let gyro_angle_rate = read_gyro_angle_rate() * 200000 / 32768
    let gyro_angle_change = gyro_angle_rate * delta_t_ms / 1000
    return (49 * (old_angle + gyro_angle_change) + accel_angle) / 50
}

function motor_coast() {
    pins.digitalWritePin(DigitalPin.P15, 0)
    pins.digitalWritePin(DigitalPin.P16, 0)
}

function motor_move(left: number, right: number) {
    pins.servoWritePin(AnalogPin.P15, 90 - right)
    pins.servoWritePin(AnalogPin.P16, 90 + left)
}

input.onButtonPressed(Button.A, function on_button_pressed_a() {
    TARGET_ANGLE = TARGET_ANGLE + 5
})

input.onButtonPressed(Button.B, function on_button_pressed_b() {
    TARGET_ANGLE = TARGET_ANGLE - 5
})

input.onButtonPressed(Button.AB, function on_button_pressed_ab() {
    basic.showNumber(TARGET_ANGLE)
})

if (InvMPU.find_mpu()) {
    basic.showIcon(IconNames.Happy)
    InvMPU.reset_mpu(5, 5)
    basic.pause(100)
    InvMPU.set_gyro_bias(5, 1, -13)
    control_loop()
} else {
    basic.showIcon(IconNames.No)
}

November 18, 2017

Self-balancing robot version 3 update!


It works! Now it balances on hard or soft floors for more than 30 minutes!



What changed?

Firstly, 2xAA rechargeable Eneloop batteries are insufficient to power those FS90R motors. The specification of the motor allows up to 6V. Two cells provide only around 2.7V total. I switch to 4xAA Eneloop batteries, providing around 5.4V. The motors were much much faster, more than twice as fast. The immediate consequence of this is that the PID parameters need to be re-tuned and scaled down to 10-20% of the values for 2xAA batteries.

Secondly, I turned up the builtin digital low pass filter in the MPU-9250 aggressively. Now it is set to 5 for both accelerometer and gyroscope. That translates to a sensor delay of 17ms and the 3dB bandwidth of about 10Hz. That is probably averaging around 17 samples at 1kHz sample rate. And that did the trick!

Here's a graph of motor power, gyroscope angular velocity and tilt angle.


The filtered angular velocity seldom exceeds 0.3 degrees per second. The filtered tilt angle seldom exceeds 1 degree. The motor power is mostly less than 10% of maximum power.

I've updated the InvenSense MPU-9255/MPU-9250/MPU-6500 module. You can now specify the low pass filter level for the accelerometer and gyroscope in the call to InvMPU.reset_mpu(). Copy this new version into your custom.ts.

The only changes from the version 1 code are the PID tuning parameters and the call InvMPU.reset_mpu(5, 5).

See the previous post on the parts for building version 3.



November 16, 2017

Self-balancing robot version 3


Version 1 was made out of Lego. It keeps breaking into pieces, the motors were not secured and it also contained the unnecessary Kitronic servo:lite board. So, I rebuilt it.

Parts:

The code for this version is identical to the code for version 1 except the tweek for a different orientation of the MPU-9255. With 2x AA batteries, identical PID parameters will work. With 4x AA batteries, the parameters have to be retuned.

But, alas, it still doesn't balance well on smooth hard floors!

It works fine on carpet or cushion even though it is only half the height of the version 1 robot. It just won't work on wood or marble floors!

After much tuning, I decided to record the motor response. I added two more wheels and turned the bot into a "car". Then I recorded the horizontal acceleration using the accelerometer in response to various motor control like a 1 second 66% power pulse. Here is a typical result with 2x AA batteries:


Why is the acceleration oscillating around zero? The amplitudes of the oscillations are much worse with 4x AA batteries.

Oh, the motor controller in the servo is a closed loop controller!

The closed-loop controller inside the FS90R is probably a variant of the standard PID controller. The oscillations are caused by the controller trying to match the output voltage to the pulse duration.

The accelerometer and gyroscope are picking up physical feedback from the controller's actions. On soft floor like carpet, the feedback is dampened physically. On hard floors, the sensors pick up random spikes that throws the PID algorithm off.

On top of that, the accelerometer only picks up the increase in motor speed after about 60ms!

In short, don't build a self-balancing robot using servo motors. Just use DC motors.

Update: I've fixed the problem and got it to work on hard floors too!