BLDC Controller phase 5: control

In the last post in this series I had just moved the Pico-BLDC code to C but it still wasn’t a fully-fledged motor controller. It would just run the motors at full speed forwards or backwards, keeping the phase of the magnets in lockstep with the rotor.

To make the driver useful, the next step was to add a control loop. To control a BLDC motor’s speed efficiently, the control loop needs to do a few things:

  • Keep the phase of the magnetic field 90 degrees ahead of or behind the position of the motor.
  • Vary the strength of the magnetic field to vary the force (and hence the torque).
  • Decide how much torque is needed. The simplest option would be to just control power like a throttle but I wanted the Pico to govern the speed of the motor.

I already had code to control the phase, so how do I control the throttle? All that’s needed is to apply a scale factor to the PWM values. Scale them all down and the effective voltage going to the motor goes down along with it’s torque.

How do we decide what throttle to set? I started with the absolute simplest approach just to prove to my self that it’d work:

  • Measure motor speed by comparing current sensor reading to last and dividing by time taken.
  • If going too slowly, add 1 to PWM scale factor.
  • If going too fast, subtract 1.

This worked (poorly as expected) but it proved that I had all the parts needed.

The next step was to implement PID control, I started with proportional control and iterated until I found something I was happy with. Really felt like I was getting somewhere at this point!

Reading the BNO08x IMU (from Golang)

We’re very fond of having an IMU on the robot. In the past we’ve used an IMU with a rate gyro, which tells you how fast the unit is rotating at any given instant. You then have to integrate the rate to get the actual angle. The problem with rate gyros is that any error in the rate gets integrated too, which means that your compass gradually drifts. This is a pain to deal with!

When we were looking for an IMU this time, Lance spotted Adafruit’s BNO08x breakout board; the chip it contains is actual magic. It has a rate gyro inside, but it runs all the integration and error cancelling onboard.

Even better than that, the BNO08x has an “easy mode” called UART-RVC, which stands for “Remote Vacuum Cleaner” mode. If you’re building something that looks like a remote vacuum cleaner (i.e. ground dwelling robot), you can put it in RVC mode by pulling a particular pin high and then there’s no configuration or callibration required. Simply connect the board to the Pi’s UART and it will send a 3-axis heading and accelerometer reading 100 times a second.

We’re reading the packets from the UART from Golang using the `”go.bug.st/serial” library. After enabling the Pi’s GPIO UART using the device overlay, we open the serial port and wrap it with a buffered reader for efficiency and to allow peaking ahead:

mode := &serial.Mode{
	BaudRate: 115200,
}
s, err := serial.Open("/dev/ttyAMA0", mode)
if err != nil {
	panic(err)
}
br := bufio.NewReader(s)

The packets from the chip always start with two bytes 0xAA 0xAA, so we wait for those; peaking ahead 2 bytes and then discarding 1 byte if we don’t see the correct bytes:

fmt.Printf("Resync...")
for {
	fmt.Printf(".")
	buf, err := br.Peek(2)
	if err != nil {
		panic(err)
	}
	if bytes.Equal(buf, []byte{0xAA, 0xAA}) {
		break
	}
	_, _ = br.Discard(1)
}
fmt.Printf("\nGot packet start.")

Then we loop, reading the 19-byte packets. We check each packet to make sure it starts with the header bytes and ends with the correct checksum…

const packetLen = 19
buf := make([]byte, packetLen)
var i int
for {
	_, err := io.ReadAtLeast(br, buf, packetLen)
	if err != nil {
		panic(err)
	}
	if !bytes.Equal(buf[:2], []byte{0xaa, 0xaa}) {
		fmt.Println("Lost sync.")
		goto resync
	}
	//fmt.Printf("Packet: %x\n", buf)
	var checksum uint8
	for _, b := range buf[2 : packetLen-1] {
		checksum += b
	}
	if buf[18] != checksum {
		fmt.Printf("  BAD CHECKSUM %x != %x\n", buf[18], checksum)
		goto resync
	}
	var report IMUReport
	report.Index = buf[2]
	report.Yaw = int16(binary.LittleEndian.Uint16(buf[3:5]))
	report.Pitch = int16(binary.LittleEndian.Uint16(buf[5:7]))
	report.Roll = int16(binary.LittleEndian.Uint16(buf[7:9]))
	report.XAccel = int16(binary.LittleEndian.Uint16(buf[9:11]))
	report.YAccel = int16(binary.LittleEndian.Uint16(buf[11:13]))
	report.ZAccel = int16(binary.LittleEndian.Uint16(buf[13:15]))
	if i%10 == 0 {
		fmt.Printf("Report: %s\n", report.String())
	}
	i++
}

It took me days to bring up the last Gyro we used, this one took about 30 minutes and its accuracy/resistance to drift looks great. With it sat on my desk, it detects a 0.2 degree rotation of the desk surface when I lean forward to type; magic!


$ go run ./cmd/imutests/
Resync....
Got packet start.
Report: 1f Y: -32.18 P:  -0.94 R:  18.85 X:  -3.24 Y:  -0.16 Z:   9.48
Report: 29 Y: -32.18 P:  -0.94 R:  18.82 X:  -3.24 Y:  -0.16 Z:   9.51
Report: 33 Y: -32.18 P:  -0.93 R:  18.82 X:  -3.24 Y:  -0.16 Z:   9.51
Report: 3d Y: -32.18 P:  -0.93 R:  18.82 X:  -3.24 Y:  -0.16 Z:   9.44
Report: 47 Y: -32.18 P:  -0.93 R:  18.82 X:  -3.20 Y:  -0.16 Z:   9.55
...

The complete code for the test program is on github.