Welcome to documentation for the Miniature Unicycle Firmware¶
Toolchain¶
PlatformIO¶
To build the code, you’ll need to install PlatformIO. This is used as a a replacement for the Arduino IDE, which has a number of shortcomings:
- Poor dependency management - using other people’s code is tricky, and specifying versions of their code is even harder
- No command-line interface - downloading code requires clumsy clicking.
The “Core” version of PlatformIO is sufficient, as that gives you the command line tools.
Once installed, open a terminal and type pio run
. This should install all
the libraries we depend on, and compile the code.
To upload the program, run pio run -t upload
. See the notes about
Flashing a program for hardware-related issues with this opperation.
Protobuf¶
To send messages from the robot to the PC, we use protobuf.
When building the code with pio run
, the command protoc --nanopb_out ...
will be executed. This requires that protoc
, the protobuf compiler, is on
the path, and that the nanopb plugin is also installed.
Documentation¶
The HTML version of this documentation is generated with Sphinx. To generate the help from C++ comments, both Breathe and Doxygen are required.
Once everything is installed, typing make html in the documentation directory should rebuild the documentation.
However, this is not necessary - the documentation will be automatically built by Read the Docs whenever this repository is pushed to.
Hardware¶
The controller for the robot is a chipKIT Max32, which is an arduino-compatible board with a PIC32 microcontroller on board. The specific microcontroller on the board is a PIC32MX795F512, which is relevant when looking for specific hardware features such as timers and PWM.
Power comes from a 7.4V Li-ion battery.
Pin assignments¶
The following is autogenerated from the source code. Pin numbers correspond
to those printed on the board. Within the code, these can be accessed with
pins::TT_DIR
. These can be passed to functions like
io::timer_for()
and io::oc_for()
in order to convert them
into peripheral objects.
Compiler-verification of pin assignments¶
This project uses a novel approach to selecting microprocessor peripherals from their pin numbers.
A typical microprocessor has multiple different features available on each pin.
However, very rarely is every feature available on every pin, and the mapping
between these features and their pins is only discoverable through a very long
table in the data sheet. For instance, we might find that OC1
is available
on pin 3
.
The result is typically that the source code does not contain any reference to
pin 3
at all, and only refers to the underlying hardare, OC1
. This is
bad for maintainability, as it tells the programmer nothing about how to check
the wiring, or what to change should the wiring change.
Obviously then, we want a function to convert pin numbers into their corresponding hardware names, and this is not a hard task. But this creates a new danger - it encourages the programmer to change pin a pin to one that no longer supports the features needed! This kind of mistake can be caught at run-time, but when programming takes a few minutes, or getting feedback is difficult, this is not acceptable.
C++11 introduces a new keyword called constexpr. When attached to a variable, this tells the compiler that its value must be calculated at compile-time. The trick then, is to produce a function that for a valid pin, is calculable at compilation-time, and for an invalid pin, is not calculable at compilation-time. We capitalize on the fact that the compiler only cares about the compile-time-calculability of the code-path it takes:
// note: no constexpr
int failure(const char* msg) { report_error(msg); while(1); }
constexpr int must_be_even(int i) {
return i % 2 == 0
? i // calculable at compile-time
: failure("Not even"); // not calculable at compile-time
}
This almost works as intended:
constexpr int ok = must_be_even(2);
constexpr int compile_error = must_be_even(3);
int ok = must_be_even(2);
int runtime_error = must_be_even(3);
There is a isocpp paper [N3583] and a follow-up paper that details possible language changes and workaround to the problem of this third line. The solution opted for was to encourage writing these as:
constexpr int ok = must_be_even<2>();
constexpr int compile_error = must_be_even<3>();
int compile_error = must_be_even<2>();
int compile_error = must_be_even<3>();
[N3583] | Scott Schurr, Exploring constexpr at Runtime, 2013. https://isocpp.org/files/papers/n3583.pdf |
API documentation¶
-
namespace
io
¶ Devices
These variables just turn the long
#define
d names into C++ references of the approapriate types-
constexpr p32_oc &
oc1
= *reinterpret_cast<p32_oc*>(_OCMP1_BASE_ADDRESS)¶
-
constexpr p32_oc &
oc2
= *reinterpret_cast<p32_oc*>(_OCMP2_BASE_ADDRESS)¶
-
constexpr p32_oc &
oc3
= *reinterpret_cast<p32_oc*>(_OCMP3_BASE_ADDRESS)¶
-
constexpr p32_oc &
oc4
= *reinterpret_cast<p32_oc*>(_OCMP4_BASE_ADDRESS)¶
-
constexpr p32_timer &
tmr1
= *reinterpret_cast<p32_timer*>(_TMR1_BASE_ADDRESS)¶
-
constexpr p32_timer &
tmr2
= *reinterpret_cast<p32_timer*>(_TMR2_BASE_ADDRESS)¶
-
constexpr p32_timer &
tmr3
= *reinterpret_cast<p32_timer*>(_TMR3_BASE_ADDRESS)¶
-
constexpr p32_timer &
tmr4
= *reinterpret_cast<p32_timer*>(_TMR4_BASE_ADDRESS)¶
-
constexpr p32_timer &
tmr5
= *reinterpret_cast<p32_timer*>(_TMR5_BASE_ADDRESS)¶
-
constexpr p32_i2c &
i2c1
= *reinterpret_cast<p32_i2c*>(_I2C1_BASE_ADDRESS)¶
-
constexpr p32_i2c &
i2c2
= *reinterpret_cast<p32_i2c*>(_I2C2_BASE_ADDRESS)¶
-
constexpr p32_cn &
cn
= *reinterpret_cast<p32_cn*>(_CN_BASE_ADDRESS)¶
Helpers
These functions allow conversion between devices and relevant configuation needed to use them elsewhere. These prevent a device having to be referred to by name in more than one place.
These should all be evaluated at compile time!
-
constexpr int
irq_for
(const p32_timer &tmr)¶ Get the interrupt bit number for a given timer.
-
constexpr int
irq_for
(const p32_cn &cn)¶ Get the interrupt bit number for the change notice hardware.
-
constexpr int
vector_for
(const p32_timer &tmr)¶ Get the interrupt vector number for a given timer.
-
constexpr int
vector_for
(const p32_cn &cn)¶ Get the interrupt vector number for the change notice hardare.
-
constexpr p32_timer &
timer_for
(uint8_t pin)¶ Get the timer connected to a given CK (clock) pin.
-
constexpr p32_oc &
oc_for
(uint8_t pin)¶ Get the output compare (PWM) connected to a given pin.
-
constexpr p32_i2c &
i2c_for
(uint8_t scl, uint8_t sda)¶ Get the I2C connected to a given pair of pins.
Compile-time Helpers
Like the above functions, but with arguments re-expresed as template parameters to force errors at compile time. These make it impossible to choose an invalid pin.
-
template <uint8_t pin>
p32_timer &timer_for
()¶
-
template <uint8_t pin>
p32_oc &oc_for
()¶
-
template <uint8_t scl, uint8_t sda>
p32_i2c &i2c_for
()¶
Functions
-
template <typename T>
Tfailed
(const char *)¶ Helper function for when an invalid argument is supplied.
-
constexpr p32_oc &
Flashing a program¶
The trace under JP5
has been deliberately cut on the board. This jumper
connects the DTR
line of the RS232 interface (from the FTDI232RQ chip that
tunnels RS232 over USB), to the reset pin of the microcontroller. The flash
programmer normally uses this line to send the chip into the bootloader, so its
code can be changed.
Unfortunately, this line is also unconditionally asserted when plugged into a
computer, making it impossible to attach to a long-running program. The fact
that this is a problem at all is a design flaw in the Max32 - were the CTS
line used instead, there would be no problem.
To reprogram the board then, JP5
must be temporarily closed. This could be
done with a jumper, but this requires that the top layer of circuit board be
removed. In practive, this is best done using a screwdriver to short the two
pins.
Danger
Do not reprogram the board while the battery is attached! Shorting JP5
is clumsy, and has a risk of shorting other parts of the board. Your USB port
is probably protected against this, but the battery may
do unspeakably bad things.
Motors¶
There are a pair of motors on the unicycle, driver with some custom and unfortunately poorly-documented hardware. The interface to the microcontroller is a pair of pwm lines, one for the forward direction, and one for the reverse direction.
Each motor is a Maxon 110134 with a Maxon 134158 gearbox attached. These motors also have attached Encoders.
Functions
-
void
setMotorTurntable
(float cmd)¶ Set the speed of the turntable.
- Parameters
cmd
: The fraction of maximum speed, in [-1 1]. Positive is counter-clockwise around the positive Z axis
-
void
setMotorWheel
(float cmd)¶ Set the speed of the wheel
- Parameters
cmd
: The fraction of maximum speed, in [-1 1].
-
void
setupMotors
()¶ Initialize the timers and PWM needed for the motors.
-
void
beep
(float freq, int duration)¶ Play a tone, using the turntable motor.
Not all frequencies resonate well. The built in
<ToneNotes.h>
is handy for turning note names to frequencies. Octaves 6 and 7 are loud and audible.- Parameters
freq
: The frequency in Hzduration
: The duration in milliseconds
Sensors¶
There is a limit switch attached to the top of the robot, for use as basic
human input. This switch must be connected to a CN
pin, as only those
pins support pull-up resistors.
Inertial¶
The robot has a combined accelerometer and gyro board that is sold by Sparkfun. The accelerometer is an ADXL345, and the gyroscope is an ITG-3200.
Both of these sensors use the I2C protocol - in fact, they share a bus. Unfortunately, the builtin arduino Wire interface that implements this protocol does not appear to work on our microcontroller board.
Functions
-
void
gyroAccelSetup
()¶ Initialize the connection to the accelerometer and gyro.
-
template <typename T>
Vector3<T>chipToRobotFrame
(Vector3<T> v_chip)¶ Convert from the chip coordinate frame to the robot frame.
The robot frame has x pointing forwards (the side with the microcontroller) z pointing left y pointing up
-
template <typename T>
Vector3<float>gyroRawToSI
(Vector3<T> raw)¶ Convert from the raw chip reading to radians per second, in the robot frame.
-
template <typename T>
Vector3<float>accRawToSI
(Vector3<T> raw)¶ Convert from the raw chip reading to meters per second squared, in the robot frame.
-
Vector3<int16_t>
accelReadRaw
()¶ Read the raw values of the accelerometer, in internal frame and units.
-
Vector3<float>
accelRead
()¶ Get the acceleration in m s^-2, in the robot frame.
-
Vector3<int16_t>
gyroReadRaw
()¶ Read the raw values of the gyroscope, without subtracting initial values.
-
Vector3<float>
gyroCalibrate
(int N)¶ calibrate the offset for the gyro. Returns the stdev of each component
-
Vector3<float>
gyroRead
()¶ Get the angular velocity in the robot frame.
-
quat
accelOrient
(Vector3<float> acc)¶ Get the robot orientation based on the accelerometer reading. Only accurate when static
-
quat
accelOrient
()¶
Encoders¶
The robot has an encoder on each motor. These go via some circuitry on the board that convert them into two lines - a tick, and a direction.
We count the ticks using the builtin hardware timers, but in order to deal with the direction reversing, we have to monitor the direction pin. We use the “change notifier” hardware to fire an interrupt whenever these pins change, and correct the sign accordingly.
Each encoder is a Maxon 201937, “Encoder MR, Type M, 512 CPT, 2 Channels, with Line Driver”.
Functions
-
void
setupEncoders
()¶ Initialize the hardware required by the encoders.
-
void
resetEncoders
()¶ Reset the counts of the encoders.
-
wrapping<uint16_t>
getTTangle
()¶ Get the angle of the turntable, in encoder ticks. Increases with counter-clockwise in the vertical axis
-
wrapping<uint16_t>
getWangle
()¶ Get the angle of the wheel, in encoder ticks. Increases with forwards motion
Communication protocol¶
Terminal¶
To open the terminal to connect to the robot, run:
cd tools
python3 terminal.py
You’ll be greeted with a yauc>
prompt, into which you can type help
for more information.
A typical session looks as follows:
...\tools> .\terminal.py
Yaw Actuated UniCycle command line interface
Connected!
Yaw Actuated UniCycle startup
Starting PWM setup
Starting I2C setup
Starting encoder setup
All done
yauc> policy ../ctrl.mat
controller {
wheel {
k_pitch: 1.2732395447351628
}
turntable {
k_dyaw: -0.6366197723675814
}
}
yauc> go 5
Starting bulk mode
Asking for logs
Waiting for reply
No data yet
Asking for logs
Waiting for reply
No data yet
Asking for logs
Waiting for reply
Test completed
Sending test data
Success
Saved rollout of 5 steps to ..\logs\2017-04-04 10꞉25꞉50\0.mat
yauc> disconnect
Connection lost
yauc> ^D
Exiting cli and stopping motors
Already disconnected
yauc>
After using the go
command, the USB cable can be disconnected. The terminal will automatically reconnect to and recieve data from the robot when the USB cable is reattached.
Matlab interface¶
Unfortunately, there is no stable implementation of protobuf for matlab. Instead, the python terminal outputs .mat files. These contains arrays of structs in the msg
variable, with the fields in the structs matching the names of the protobuf fields.
Rather than reading these files directly, we expose the same interface as PILCO_:
-
rollout
(start, ctrl, H, plant, cost, verb)¶ Shadows the PILCO method. Writes ctrl.mat with the necessary information to load in the terminal, and then brings up a file/open dialog to load the logs from the robot.
Parameters: - start – Ignored
- ctrl – Contains the policy parameters. Currently assumed to be a linear controller or a random one. When passed a random one, a hand-crafted deterministic controller is used instead.
- H – The number of steps to run for. Assumed to be 50.
- plant – Information about the plant. Provides
.dt
,.in_frame
, and.out_frame
, which are useful for converting to and from protobuf field names - cost – As in the normal rollout, produces costs from the states
- verb – Ignored