RC Receiver to SPI interface

In the design of C2Bot, I wanted to use a Remote Control (RC) transmitter to drive the robot. This meant reading 6 or more PWM (or PPM) channels from an 8-channel RC receiver (Turnigy 9X). Given the tight timing required, it would be unreasonable to expect that a Raspberry Pi could sample and read the receiver pins itself. Instead, I used a $3 ATtiny167 board (Digispark Pro) to measure the PWM signals and provide a simple SPI interface for the Raspberry Pi to read. The Raspberry Pi then implemented a differential drive algorithm to convert the joystick inputs to motor commands.

Radio Control (RC) pin signaling

RC transmitters convert a set of potentiometers (joysticks) and switches into an encoded
signal that is suitable for wireless transmission (72MHz, 2.4GHz, etc.) using various technologies (such as signal hopping, spread-spectrum) that increase resiliency
against data errors. Multiple channels share the same transmission frequency through
interleaving.

The RC receiver is responsible for de-interleaving and decoding this transmission
into a multi-channel format suitable for connection to a servo, ESC or flight controller. There are many output formats provided by RC receivers today including: PWM, PPM,
PCM, SBUS and DSM2/DSMX. By far the most common format is PWM.

8-Channel PWM stream from RC receiver (Turnigy 9X8Cv2)

An RC receiver with PWM outputs will have a set of servo headers (one set per channel) that can directly
connect to a servo or flight controller input. The 3-pin servo interface simply provides power (+5V),
ground and signal. The signal is usually a stream of pulses that are 1-2ms wide, repeating at a rate of
50 pulses per second (20ms period). Servo motors have internal circuitry that decodes this pulse stream
and converts it into a target rotation angle for the motor shaft's output. PID feedback logic continuously directs
the motor to adjust its position (read by an internal potentiometer) to match the target position.

Decoding RC PWM signal by Arduino / ATtiny Microcontroller

For C2Bot, I wanted the Raspberry Pi to read the Radio Control transmitter's
joysticks, switches and knobs and use these to control the robot motors, a 6 DoF robot arm as well as the operational modes. To do this, I decided to use an ATtiny167 microcontroller (part of a $3 Digispark Pro board) to sample the PWM signals directly and send the measured pulses across a SPI interface to the Raspberry Pi. Prior to selecting SPI, I had considered a few other options in sending data between an Arduino and the Raspberry Pi.

Connecting the AVR to the RC receiver and Raspberry Pi

For this setup, we'll wire up the RC receiver, the ATtiny, a level shifter and the Raspberry Pi. Assuming that the RC receiver requires Vcc=5V to operate, we'll want to also use a 5V supply voltage for the Arduino / ATtiny. Since the microcontroller only permits a maximum of Vcc+0.5V on the input pins, operating the AVR at 3.3v and the receiver at 5.0v would potentially damage the AVR. As we will be connecting the AVR SPI bus to the Raspberry Pi (natively 3.3v), we'll have to use a level shifter circuit.

The following is an example set of connections between the AVR and the other two devices:

RC Receiver Pin

ATtiny Pin

Raspberry Pi Pin

ATtiny Usage

CH1 Signal

#0 / PB0 / SDA

RC signal

#1 / PB1

Debug output (also LED on Digispark Pro)

CH2 (Signal)

#2 / PB2 / SCL

RC signal

CH3 (Signal)

#3 / PB6 / USB+

RC signal

CH4 (Signal)

#4 / PB3 / USB-

RC signal

CH5 (Signal)

#5 / PA7

RC signal

#6 / PA0 / RX

TX

Debug output, serial UART or extra RC channel

#7 / PA1 / TX

RX

Debug output, serial UART or extra RC channel

#8 / PA2 / MISO

MISO

SPI MISO

CH6 (Signal)

#9 / PA3

RC signal

#10 / PA4 / MOSI

MOSI

SPI MOSI

#11 / PA5 / SCK

SCLK

SPI Clock

#12 / PA6 / SS

CE0

SPI Slave Select

BAT+

VCC=5v

Receiver & ATtiny at 5v

3v3

RPi at 3.3v

BAT-

GND

GND

Grounds are common

NOTE: All connections between ATtiny and RPi are via level shifter circuit

Sampling the PWM signals

Direct extraction of Receiver's SPI interface

In some cases, it may be possible to open up the RC receiver itself and tap into the local signals
and extract the internal SPI bus. In the case of the Turnigy 9X / 9X8Cv2, there is indeed an internal SPI
bus that could be extracted, but I didn't want to hack my receiver. I thought it would be more useful for
others to provide a solution that didn't involve cutting it open and additional soldering. That said,
by directly interfacing to the internal SPI bus one can increase accuracy by removing potential errors
that occur in measuring and sampling the PWM signals.

Sampling of RC PWM output pins in ATtiny167

After connecting the receiver's signal pin of each RC channel to a GPIO port on the Arduino / ATtiny / AVR microcontroller and ensuring that we have a common ground signal, we are ready to start sampling. There are several methods of sampling these PWM signals on the AVR:

Polling with PulseIn()
The PulseIn function polls the GPIO input pin and attempts to measure the time between two successive edges (eg. rising then falling). This function is blocking, causing all other functionality to be suspended. It may be OK to use PulseIn if you are only measuring a single receiver channel, but it is very unsuitable in handling multiple channels (since the PulseIn for one channel will mask any edge events that occur on other channels). Furthermore, the main code loop is blocked from performing any other useful work.

Using Pin Change interrupts
By far the most common method, a Pin Change ISR is attached to a specific input pin. One or more input pins may share the same Pin Change Interrupt (eg. PCINT0). Whenever a rising or falling edge occurs on a pin associated with the PCINT, the PCINT ISR (Interrupt Service Routine) is called. The ISR is responsible for detecting which pin had a transition as well as determining which edge (ie. rising or falling) triggered the ISR callback. By latching the current time (eg. micros()) on a pin's rising edge and and again on the falling edge event, we can estimate the pin's pulse width by subtracting the two timestamps. Because the measurements are
all handled by the ISR, the main code loop is free to perform other functions.

Tight GPIO sampling loop
Although the Pin Change Interrupt methodology appears to be the most suitable methodology,
there are some drawbacks when it comes time to make a reliable SPI slave interface on the
ATtiny. The problem is that a PCINT may occur at any time -- in the worst case, one or more
Pin Change interrupts may occur immediately prior to a SPI transfer complete interrupt (SPI_STC).
Since the SPI interrupt is held off until the other interrupts have completed, we may
run out of time to set up the SPI Data Register (SPDR) ahead of the next SCK from the master.
The net result is often data corruption! (generally marked by a SPI Write Collision (WCOL) error event).

As an alternative, we can instead write a tight loop that continuously samples each of the GPIO pins, recording timestamps as needed. Unlike the PulseIn() function, we don't actually wait for any edges -- instead we test the pin status and only if an edge was detected do we record the timestamp / duration. The reason why this may be beneficial to the Pin Change Interrupt strategy is that we avoid causing any additional interrupts on the AVR that may otherwise delay / pre-empt the SPI transfer complete interrupts.

SPI Protocol and Data Transfers

The timing associated with SPI transfers is totally owned by the SPI master. Once a SPI master has completed the writing of a byte towards the slave, it will wait a short period of time before initiating the next byte. In a read transaction, the master typically issues a command and address in the first byte(s) and then expects the slave to return one or more bytes as a response immediately afterwards.

The SPI master asserts a slave select (SS / CE) to notify a single slave that it will be the target of an upcoming transaction. Then it drives the SPI clock (SCLK) to step through each bit of the full-duplex data transfer. The MOSI pin (Master Out, Serial In) sends data from the master to the slave (ie. Arduino to Raspberry Pi) and MISO in the opposite direction.

The manner in which SPI devices communicate addresses and read/write commands is not defined by the basic SPI protocol. Instead, it is up to the SPI slave peripheral to document how it expects to see various transaction types. One example of a simple transfer protocol definition is the following:

Cycle

16-bit SPI write transaction

16-bit SPI read transaction

Byte

MOSI

MISO

MOSI

MISO

1

{00}{Addr[5:0]}

(Don't-care)

{01}{Addr[5:0]}

(Don't-care)

2

{WrData[15:8]}

(Don't-care)

(Don't-care)

{RdData[15:8]}

3

{WrData[7:0]}

(Don't-care)

(Don't-care)

{RdData[7:0]}

Example of good SPI read transaction

Successful SPI Read Transaction at 62kHz

In the above example, we see a 3-byte SPI transaction. In this particular
protocol, the first byte is used as a command/address cycle. The next two bytes are the read or write data for the transaction. The first byte has a value of 0x4E, indicating a Read to [byte] address 0x0E. The slave returns a response of 0x05 and 0xDC, which is 0x05DC (1500 decimal) when interpreted as a 16-bit unsigned value. The 0x99 and 0x88 from the Master in bytes 2 and 3 are dummy values and are ignored by the slave.