Decoding FrSKY telemetry data with an Arduino

I used a FrSKY D4R-II receiver and a DHT DIY Tx module with an Arduino Pro Mini for the purpose of this post.  These FrSKY products use the older “D mode” telemetry and the DHT module is now discontinued, but I’ll be updating this post to deal with the newer S.Port Telemetry soon.

 

There are still lots of people using the older “D mode” equipment and in any case the Arduino code shows the general approach to decoding serial data on any digital pin.  The code presented should work on any of the ATmega-based Arduinos: Uno, Nano, Pro Mini and so on…

First thing to be aware of is that the Telemetry data where it emerges from the transmitter module on the Txd pin is designed to be connected to a PC-type RS232 port.  It works at voltage levels of about +6V swinging to -6V.  Don’t connect it directly to your Arduino!  It could damage either the module or the Arduino!

Fortunately the only ‘interface’ needed to connect the signal to the Arduino is a humble 100k resistor.  This limits the maximum current flow to 60 microamps and the ATmega’s protection diodes on each I/O pin then safely clamp this to the Arduino’s 0V to 5V range.  This is a method approved by the chip manufacturers, Atmel – indeed they even suggest that mains voltages can be connected to the chip through a suitable limiting resistor in an application note: http://www.atmel.com/Images/Atmel-2508-Zero-Cross-Detector_ApplicationNote_AVR182.pdf

So for the purpose of this post we connect the signal from Txd through a 100k resistor to Arduino Pin 11 (you can choose any digital or analogue pin with small modifications to the code), and you need to connect the GND pin from the module to GND on your Arduino.

We use setup() to initialize an interrupt handler to respond to changes of signal on pin 11, to initialize a few variables and start a Serial port where we can display things:

void setup(void) {
  pinMode(11, INPUT); // serial input (Digital 11 is Port B, bit 3)
  PCMSK0 = 0x08; // set mask to allow (only) digital pin 11 pinchange interrupts on Port B
  PCICR |= 0x01; // allow pinchange interrupts for Port B
  rssi = a1 = a2 = 0;
  timeout = 1000; // ms
  Serial.begin(115200);
  oldMillis = millis();
}

The interrupt handler decodes the incoming serial characters (remember the mark and space levels are swapped compared to standard Arduino serial).  Don’t bother understanding this code unless you’re specially interested – all it does is decode individual 8-bit characters arriving and call handleRxChar() to handle each individual character that arrives:

ISR(PCINT0_vect) { // Port B pinchange interrupt
  static uint8_t prev = 0;
  static uint8_t currChar = 0;
  static uint8_t currBits = 0;
  uint8_t now = TCNT0;
  sei(); // allow other interrupts once time has been captured
  uint8_t bits = (((uint8_t)(now - prev)) + 13) / 26; // at 9600 baud with 4 uS TCNT, one bit is 1E6 / (4 * 9600) = 26.04 counts, so half a bit is ~13 counts
  if (!bits || bits > 9) { // detect 0 bits when gap between characters and TCNT0 wrapped exactly, also limit bits to 9 to prevent overflow of (currBits + bits)
    bits = 9;
  }
  prev = now;
  if (!(PINB & 0x08)) { // -ve edge, but inverted RS232 so preceding data bits were low
    if (!currBits) { // start of char so bits includes the start bit (start bit is low)
      currBits = 1; // currBits count does includes the start bit
      bits--; // but don't include start bit in character decode
    }
    currChar >>= bits; // received low bits represent '0' in character.  LSB arrives first, MSB last
    currBits += bits;
  }
  else if (currBits) { // preceding data bits were high
    if (currBits + bits > 9) { // trim stop bit if receiving character values >= 128 (MSB will be high and stop bit is also high)
      bits = 9 - currBits;
    }
    currChar = (0xFF00 | currChar) >> bits; // received high bits represent '1' in character.  LSB arrives first, MSB last
    currBits += bits;
  }
  if (currBits > 8) { // start bit plus character received
    uint8_t rxChar = currChar; // don't know if interrupt is re-entrant - if not then this won't help but if it is then allows next bit(s) to be received while handling char
    currChar = currBits = 0;
    handleRxChar(rxChar);
  }
}

Now we come to the FrSKY specific stuff – FrSKY data arrives in 9-byte packets framed by hex 7E characters. Of course the value 7E might be included in the data, but can’t be sent directly as it would be mistaken as a framing byte. To get around this, FrSKY uses a technique called byte stuffing where 7E is replaced by the pair 7D 5E. That means that 7D now also can’t be sent directly as a data byte as it would be mistaken for the character introducing byte stuffing – so 7D is replaced by the pair 7D 5D. This code replaces any pair of byte stuffing codes with the single byte original and when it sees a full 9-byte data packet framed with 7E it passes that packet on to handlePacket() for further processing:

void handleRxChar(uint8_t b) { // decode FrSky basic telemetry data
  static uint8_t packetPosition = 0;
  static uint8_t packet[9];
  static bool byteStuffing = false;

  if (b == 0x7E) { // framing character
    if (packetPosition == 9) {
      handlePacket(packet);
    }
    packetPosition = 0;
    return;
  }

  if (b == 0x7D) {
    byteStuffing = true;
    return;
  }

  if (byteStuffing) {
    byteStuffing = false;
    if (b != 0x5E && b != 0x5D) {
      packetPosition = 0;
      return;
    }
    b ^= 0x20;
  }

  if (packetPosition > 8) {
    packetPosition = 0;
  }
  else {
    packet[packetPosition++] = b;
  }
}

There may be several different packet types when telemetry sensors such as an altimeter are present, but for now we’ll ignore all but the most basic type of packet. This packet type with a signature byte of FE is always present and contains the RSSI (received signal strength indication) and the two analogue voltages AD1, and AD2. As well as the signal strength received by the receiver, this packet also contains the signal strength received by the transmitter – but we’re not usually interested in that – we’re interested in how well the receiver can “hear” the transmitter and not the other way round! The handlePacket function in this example just extracts the byte values for rssi, a1, and a2 and makes them available as global variables.

void handlePacket(uint8_t *packet) {
  if (packet[0] == 0xFE) { // not interested in telemetry data other than the most basic kind for this application
    a1 = packet[1]; // A1: 
    a2 = packet[2]; // A2:
    rssi = packet[3]; // main (Rx) link quality 100+ is full signal  40 is no signal
    // packet[4] secondary (Tx) link quality.  Strength of signal received by Tx so not particularly useful.  Numbers are about double those of RSSI.
    cli(); timeout = 1000; sei();
  }
}

All that’s left is to display any received data by printing it to the Serial port so it can be monitored by the Arduino terminal (Ctrl-Shift-M and set the baud rate to 115200). The loop() here uses the millis() millisecond counter to only update the serial display twice per second:

void loop(void) {
  uint32_t now = millis();
  int32_t ms = now - oldMillis; // milliseconds elapsed since last loop
  oldMillis = now;
  
  if (timeout <= (int16_t)ms) {
    timeout = a1 = a2 = rssi = 0;
  } else {
    timeout -= ms;
  }

  if (ms >= millisTillNextPrint) { // time to print
    if (!rssi) {
      Serial.println("No telemetry data being received");
    } else {
      Serial.print("RSSI: ");
      Serial.print(rssi);
      Serial.print(" AD1: ");
      Serial.print(a1);
      Serial.print(" AD2: ");
      Serial.println(a2);
    }
    millisTillNextPrint += 500 - ms;
  } else {
    millisTillNextPrint -= ms;
  }
}

And that’s it! The complete Arduino sketch is attached here: FrSKY_telemetryDecode

In my next post, I’ll expand on the data packet handling to explain how different types of telemetry sensor data are encoded, and how you can even send your own home-brew telemetry data back to the ground by injecting a serial data stream to the receiver on board your model.

An Arduino-based programmer for the AT89C2051 chip

The Atmel AT89C2051 is a low cost microcontroller in a 20-pin DIL package.  It runs MCS-51 (commonly termed ‘8051’) code.  It works from 2.7V to 6V at anything from 0 Hz up to 24 MHz.  It has 2K bytes of Flash memory to hold the program and 128 bytes of RAM.  It has 15 I/O lines, a UART, an analogue comparator and two 16-bit timer/counters.

I came across the chip as it’s often used in cheap 7-segment clock kits such as this one from BangGood (only £2.71 at the time of writing).

I wanted to reprogram the chip so I could use the kit as a stopwatch/timer instead of a regular clock.  Of course I could have bought a programmer to do the job, but reading the chip’s data sheet it seemed straightforward to do the programming with an Arduino – and I thought it would be a fun project to do that.

The chip is programmed a byte at a time by setting up each byte on 8 of the chip’s I/O lines and then pulsing some of the other I/O lines to ‘burn’ the byte to flash memory and move on to the next byte to be programmed.  You can also read the existing program out of a chip (unless a read-protect bit has been set) and there are special ways of pulsing the I/O lines to erase the whole chip and so on.

The only tricky thing is that one pin has to be raised from the nominal operating voltage of five volts up to twelve volts during programming – the challenge was working out the easiest way to do this using an Arduino.

So I decided to use an Arduino Mega 2560 for this project.  A Uno doesn’t have quite enough I/O to do the job properly, and the Mega 2560’s double row of I/O pins makes routing the connections to the chip simple as the chip can sit directly over the double-row connector.

I decided to use a charge pump (voltage multiplier) running off the Arduino’s five volts to generate the programming voltage – that seemed cleaner than needing a separate twelve volt supply.  It just uses a few diodes and capacitors and relies on the Arduino pulsing some of its I/O lines to drive the voltage multiplier.  A couple of zener diodes clip the voltage down to exactly 5V or 12V and a couple of transistors, also switched by the Arduino, select between either of those voltages or 0V to drive the pin on the chip.

I designed a PCB using the free KiCad package.  Here’s a .pdf of the circuit diagram, and here is what KiCad produces as a picture of the design.  In the picture it looks like the chip to be programmed is soldered straight into the board, but of course in reality a ZIF socket is fitted in that position so that the chip(s) you are programming can be quickly swapped.

programmer3DThat picture wrongly shows the tracks on the top of the board – I design them that way for home production as the transfer process mirror-images the tracks so that they’re correct for the back of the board.  If you fancy making one of your own, it would be quite straightforward to  do it on strip board – like I say most of the pins of the chip just connect direct to the Arduino pins that the chip sits over.  If you want to etch your own PCB, here is a .pdf of the mask.

Here are a couple of snaps of the prototype board.  You can see I didn’t bother to crop back the board edges!

 

topbackAnd this is what it looks like when docked on top of the Arduino Mega 2560.

topDockedbackDockedSo that’s about it for the hardware.  I’ll make a separate post about the Arduino sketch that does the work of programming the chip, and the PC program that talks to the Arduino to send and receive hex files.

Fast, small, and simple Arduino SPI RAM chip routines.

The 8-pin chip, 23LC1024, is a good candidate for adding extra RAM to simple Arduinos such as the Uno, Nano, Mini.  It provides 128K of RAM (the ATmega328 chip that these Arduinos use only has 2K of internal RAM).

The chip works at 5V (anything from 2.5V to 5.5V) and uses SPI so it only uses 4 pins of the Arduino to talk to it.  You can buy the chips on eBay for less than £5 each.

I wanted some fast simple routines to copy any arbitrary number of bytes between ‘normal’ RAM and the SPI RAM.  I didn’t want to link in any huge libraries so I wrote these small routines that hit the 328 chip’s SPI registers direct.

There are only three functions:

spiRam::start(); // sets up the SPI interface – call once from your setup() routine

spiRam::writeSpi(*ptr, address, length); // copies to SPI RAM

spiRam::readSpi(*ptr, address, length); // copies from SPI RAM

*ptr is an unsigned byte (uint8_t) pointer – but you can cast any other data type address to that easily – see example.

address is an unsigned long (uint32_t) as addresses in the SPI RAM range from 0 to 131071 (2^17 – 1).

length is an unsigned int (uint16_t) – but will never be bigger than 2048 as the 328 chip only has 2K of RAM.

Standard connections (you can use a different pin than D10 for the chip select – but there’s not much point unless you want to use more than one RAM chip to give more than 128K RAM – see notes in the .h file).

Chip pin Arduino pin
1 D10
2 D12
3 5V
4 Gnd
5 D11
6 D13
7 5V
8 5V

Here’s the sketch: SpiRam23LC1024.zip Unzip it inside your Arduino folder. You want to end up with the SpiRam23LC1024 folder in your Arduino folder with the SpiRam23LC1024.ino and the two other files inside.  Then it should compile and upload okay.  To use it with your own code just move the .h and .cpp files inside your folder where your .ino file is and add the line

#include “spiRam.h”

up at the top of your sketch.

Because the routines are interrupt driven your code could be doing something else while the transfers are taking place – but be careful if you’re not sure – it’s easy to start destroying or changing contents before they’ve finished being transferred if you’re careless. The routines are very fast anyhow (9 milliseconds to transfer 1024 bytes in either direction), so as standard there is a ‘busy wait loop’ until the transfers are complete so you don’t need to worry about checking from inside your code.

 

ATmega328P-PU 12MHz bootloader

It took me an embarrassingly long time to get a 328 with a 12MHz crystal working so that it would allow a normal (serial / FTDI) upload from the Arduino IDE.

There are a few older guides around but I didn’t find one that used the newer optiboot bootloader.

If you don’t want to compile the bootloader yourself, you can download the compiled optiboot_at328mega_12.hex file from this link (right-click and and Save (link) as…) just copy it to your optiboot folder before editing your “boards.txt” file as described below.

If you want to compile the hex file yourself here’s how:

Edit the Makefile in your optiboot folder. On my (Windows 7 64bit) machine using Arduino 1.0.5-r2 this folder was C:\Program Files (x86)\Arduino\hardware\arduino\bootloaders\optiboot

Add this section. I put it in after the existing atmega328 sections and before the Sanguino section  (note that the -W1, … shouldn’t be on a separate line – it’s a continuation of the LDSECTIONS line, but this blogging software seems to want to wrap it onto a new line):

atmega328_12: TARGET = atmega328
atmega328_12: MCU_TARGET = atmega328p
atmega328_12: CFLAGS += '-DLED_START_FLASHES=3' '-DBAUD_RATE=115200'
atmega328_12: AVR_FREQ = 12000000L
atmega328_12: LDSECTIONS  = -Wl,--section-start=.text=0x7e00 -Wl,--section-start=.version=0x7ffe
atmega328_12: $(PROGRAM)_atmega328_12.hex
atmega328_12: $(PROGRAM)_atmega328_12.lst

atmega328_12_isp: atmega328
atmega328_12_isp: TARGET = atmega328
atmega328_12_isp: MCU_TARGET = atmega328p
# 512 byte boot, SPIEN
atmega328_12_isp: HFUSE = DE
# Low power xtal (12MHz) 16KCK/14CK+65ms
atmega328_12_isp: LFUSE = FF
# 2.7V brownout
atmega328_12_isp: EFUSE = 05
atmega328_12_isp: isp

This is just a copy of the atmega328. section with:

  • the name changed from atmega328. to atmega328_12.
  • the f_cpu parameter changed from 16000000L to 12000000L
  • the two (PROGRAM) lines changed so that the .hex and .lst filenames also have the “_12” addition
  • the comment about the 16MHz crystal frequency changed to 12MHz

Run a command shell as administrator and navigate to the optiboot folder.  Enter the following command to compile the new 12MHz optiboot bootloader:

omake atmega328_12

Now edit your boards.txt file.  On my machine this was located at C:\Program Files (x86)\Arduino\hardware\arduino  Add this new section at the end – again this is just a copy of the ‘uno’ section with the few obvious edits for frequency and bootloader file.

##############################################################

atmega328bb12.name=ATmega328 12MHz crystal
atmega328bb12.upload.protocol=arduino
atmega328bb12.upload.maximum_size=32256
atmega328bb12.upload.speed=115200
atmega328bb12.bootloader.low_fuses=0xff
atmega328bb12.bootloader.high_fuses=0xde
atmega328bb12.bootloader.extended_fuses=0x05
atmega328bb12.bootloader.path=optiboot
atmega328bb12.bootloader.file=optiboot_atmega328_12.hex
atmega328bb12.bootloader.unlock_bits=0x3F
atmega328bb12.bootloader.lock_bits=0x0F
atmega328bb12.build.mcu=atmega328p
atmega328bb12.build.f_cpu=12000000L
atmega328bb12.build.core=arduino
atmega328bb12.build.variant=standard

Now launch Arduino, select “ATmega328 12MHz crystal” as your board and “Burn Bootloader” from the Tools menu.

That’s it, and you should be up and running with the normal hardware serial port working correctly with the ‘Serial’ commands and serial uploading from the Arduino IDE also working correctly.  I’ve seen comments that not all of the timing functions (microsecond delays etc.) work accurately when using a 12MHz crystal and the standard Arduino libraries.  I’ve not investigated that yet; and so far I’ve not tried using software serial with a 12MHz crystal – that also may require some tweaking…

 

 

Decoding 6 servo channel inputs with an Arduino UNO

Standard radio control receivers drive their connected servos by outputting a train of pulses.  The pulses usually repeat at a 50 Hz rate but the pulse width is the important thing – the standard is a 1.5 ms pulse width for a servo at its center position varying from about 1.0 ms up to 2.0 ms for servo’s nominal travel range. The pulses are positive going, usually about 5V in amplitude.

Searching on the web finds lots of examples of code to read the pulse widths for several servo channels, but for my application the Arduino is looking after several sensors besides the servo signals and also running software serial ports.  The examples I found all gave too much jitter and inaccuracy.

I wired the six channels to the Arduino digital pins 2 to 7.  Pins 0 and 1 are used by the UART (serial communication to PC or other device) so it’s best to avoid those.  Conveniently this means that all six channels are contained on a single Arduino input port, (Port D) so this makes the code to capture the pulse widths very clean.

We enable an interrupt that occurs when any of the 6 pins change state. To do this we set one bit for each of the 6 channels in the PCMSK register.  Then to allow changing input states on Port D to generate interrupts there is just one bit in the PCICR register to set:

PCMSK2 |= 0xFC;
PCICR |= 0x04;

The interrupt handler, declared as ISR(PCINT2_vect) checks each of the six pins. If a pin has changed state since the previous interrupt, we do one of two things:  If the pin has gone high we just remember the current time; if the pin has gone low we store the pulse width by subtracting the remembered time from the current one.  The Arduino has a function that returns microseconds (with a resolution of 4 microseconds on the UNO) convenient for this task.  The microsecond timer wraps around back to zero roughly every 70 minutes but this doesn’t cause any problems – by doing the subtractions using unsigned long integers the pulse widths are still correct even when a ‘wrap around’ occurs.  Here’s the complete program including interrupt handler and simple test output that just prints the current pulse widths to the serial port (PC).

volatile uint8_t prev; // remembers state of input bits from previous interrupt
volatile uint32_t risingEdge[6]; // time of last rising edge for each channel
volatile uint32_t uSec[6]; // the latest measured pulse width for each channel

ISR(PCINT2_vect) { // one or more of pins 2~7 have changed state
  uint32_t now = micros();
  uint8_t curr = PIND; // current state of the 6 input bits
  uint8_t changed = curr ^ prev;
  int channel = 0;
  for (uint8_t mask = 0x04; mask; mask <<= 1) {
    if (changed & mask) { // this pin has changed state
      if (curr & mask) { // +ve edge so remember time
        risingEdge[channel] = now;
      }
      else { // -ve edge so store pulse width
        uSec[channel] = now - risingEdge[channel];
      }
    }
    channel++;
  }
  prev = curr;
}

void setup() {
  Serial.begin(9600);

  for (int pin = 2; pin <= 7; pin++) { // enable pins 2 to 7 as our 6 input bits
    pinMode(pin, INPUT);
  }

  PCMSK2 |= 0xFC; // set the mask to allow those 6 pins to generate interrupts
  PCICR |= 0x04;  // enable interupt for port D
}

void loop() {
  Serial.flush();
  for (int channel = 0; channel < 6; channel++) {
    Serial.print(uSec[channel]);
    Serial.print("\t");
  }
  Serial.println();
}