online lostsh.github.io open-boat | 2020-06-19 rfc 3514: evil bit not set
post 03 2020-06-19 c99 | arduino 3 wirings

Open Boat

c99 | arduino hm-10

An RC boat on an Arduino. It is an open source boat project: simple, and easy to do. The main objective was to make a boat as simple as possible, out of almost whatever came to hand.

The Bluetooth module I used is a BLE 4.0 module, an HM-10, and it is the only slightly delicate element to get hold of. It is also the part you should feel free to change: we can easily replace it with a more classic bluetooth module (HM-05), with a wifi module, or with an RC receiver, depending on what you want and on the components you already have. Nothing else in the boat cares which one you picked.

Parts

Bill of materials, both builds
Part Build Role
Arduinobothcontroller
DC motorbothpropulsion
Servo motorbothrudder, via a bent steel pushrod
Radio modulesee wiringcommand link; the first drawing omits it
MOSFET, N-channelsimpleswitches the motor on and off
NPN transistorcontrollerreplaces the MOSFET in this variant
Diode, 1N4007controllerflyback across the motor
Resistor, 1 kohmcontrollerin series with the switching part control pin
9 V packcontrollermotor supply, separate from the logic

The controller build is the simple one plus a flyback diode, a gate resistor and its own supply, with the switching part swapped. You do not need both switching parts.

The original parts list says NPN transistor while all three drawings show an N-channel MOSFET. They disagree. The drawings are the ones with the wiring on them.

Wiring

Three versions, in the order the drawings are numbered, which is also the order in which I would build them.

Wiring diagram: an Arduino Uno with an N-channel MOSFET wired to its digital pins, driving a grey DC motor, and a blue servo connected to a PWM pin and to the 5 V and ground rails. No radio module.
The simple version, without the bluetooth module
Wiring diagram: the same Arduino Uno, MOSFET, DC motor and servo, with a small blue radio module added above the board and wired to the serial and power pins.
V1 of the system. The bluetooth is in, and the motor is on or off. Nothing between
Wiring diagram: an Arduino Uno with a module labelled Bluetooth, a resistor in series with the gate of an N-channel MOSFET, a diode across the DC motor, a servo, and a 9 V battery supplying the motor side.
With the motor controller: a resistor into the gate, a diode across the motor, a 9 V pack

The link

Bluetooth was V0. It works, it pairs in a minute, and it hands you something that behaves like a serial port: you write bytes at one end and they arrive at the other, in order, or the link is down and nothing arrives at all. That abstraction is why it is the right first choice, and it is also the whole of what it gives you. Range is short, the link is unauthenticated once paired, and every framing decision is still yours because a byte stream has no frames.

The next version moved to a 2.4 GHz transceiver module, and that changes the problem rather than improving it. A bare radio does not give you a stream. It gives you fixed-size packets, individually addressed, individually acknowledged or not, and everything above that is code you write. So I wrote the protocol.

The frame

The command frame is a C struct with an explicit field layout and explicit padding, declared packed so the compiler is not free to arrange it. Anything that goes on the wire as raw memory has to be laid out on purpose. The real field names and widths are not to hand, so what follows is the shape of the design rather than a transcription of the sketch:

illustrative layout, not the 2020 source
  struct __attribute__((packed)) frame {
      uint8_t  magic;      // frame start, cheap reject
      uint8_t  seq;        // sequence, wraps at 255
      uint8_t  flags;      // type, ack request, and spare bits
      int16_t  throttle;   // signed: astern is negative
      int16_t  rudder;     // signed: centre is zero
      uint8_t  pad;        // explicit, not compiler-chosen
      uint16_t check;      // integrity field over everything above
  };

Padding is the part worth dwelling on, because it is where this goes wrong silently. AVR is an 8-bit machine with no alignment requirement at all: every load is a byte load, so avr-gcc inserts no padding and a struct is exactly the sum of its fields. Almost every other target you might put on the far end does not behave that way. On ARM or x86 the compiler pads members out to their natural alignment, so the same declaration produces a different number of bytes with the fields at different offsets. Members are never reordered, which is the only guarantee C gives you here; where each one starts is not. The layout above is ten bytes packed and twelve padded, with a byte inserted before each of the two-byte fields that would otherwise land on an odd address. A receiver assuming the wrong one reads garbage that looks like plausible garbage.

Declaring the frame packed pins the layout to the declaration on both sides. On most architectures that trade costs you something, because unaligned access is slower or has to be synthesised. On AVR it costs nothing whatsoever, since there was no alignment to give up. Packing a wire format is close to free on exactly the machine that most needs the format to be unambiguous.

Losing packets on purpose

A 2.4 GHz link in a crowded band drops frames, and it corrupts them as well. Both failures are handled in the frame rather than around it: the format carries an integrity field and the receiver runs a standard error-handling scheme over it rather than anything invented for the occasion. That is the one part of this protocol where rolling your own would have been the wrong instinct.

What the frame carries changes how much a lost one costs. Send absolute positions and a dropped frame costs a single update, because the next one restates the true position. Send increments and every lost frame is integrated into a permanent offset that nothing later corrects. It is the cheapest robustness available on a lossy link, and it is a choice made in the frame layout rather than in the control code.

Handshake and confidentiality

On top of that sits a handshake and a lightweight cipher, both homemade. The handshake establishes a session between a remote and a boat before either will accept traffic from the other; the cipher keeps the command traffic from being read off the air by anything listening on the band.

Confidentiality is the only property a cipher gives you on its own, and it is worth being precise about the gap. Encrypting a frame stops someone reading it. It does not stop them recording one and sending it again later, because a replayed frame is a valid frame: resisting that needs a counter or a nonce in the frame and a receiver that rejects anything it has already seen. Freshness and secrecy are separate problems and only one of them is solved by encrypting.

Both are hobby constructions and worth naming as such: a scheme designed by one person and reviewed by nobody is not a security control, and on an 8-bit part with kilobytes of RAM the cheap primitives are cheap for reasons that show up under analysis. What it does buy is real all the same. It raises the cost of interfering with the boat from "own a 2.4 GHz radio" to "reverse the protocol first", which is the correct bar for a toy and the wrong one for anything else.

The boat

Overhead photograph of the finished boat sitting on grass: a hand-cut white polystyrene hull holding a small blue servo, a DC motor strapped down with white cloth tape, a bent steel pushrod running along the hull, a small blue radio module and a loom of jumper wires, with the word BAT written on the foam in pencil and a black propeller at one end of the hull.
The whole boat. Foam, cloth tape, jumper wires, and one pencil label

The hull is a block of polystyrene cut by hand, and everything else is taped or wedged into it: the servo with a bent steel pushrod running back along the hull, the motor held down with cloth tape, the radio module and its jumper wires at one end, and BAT written on the foam in pencil where the battery goes. It is ugly, and it floats.

More

For more information, visit the project's github page.