Decoding FrSKY telemetry data with an Arduino – Part four

Now let’s look at the necessary code to transmit telemetry data from our aircraft. We have an Arduino on board the plane, connected to the telemetry port of our FrSky receiver. I used a D4R-II. The harness that plugs into the side of the receiver (at least the one supplied with mine) has red wire for the serial data coming out of the receiver – this I connected to Pin A2 of my Arduino, and a green wire for serial data into the receiver – this I connected to Pin A1.

I made the Arduino mimic the way the standard FrSky telemetry options send their data – so there’s no need for a FrSky hub on the plane – all the sensors for GPS, magnetometer, accelerometer, etc. are connected to the Arduino – the Arduino reads those sensors and then sends data to the FrSky receiver in the standard form. So if you have a transmitter such as a Taranis that understands FrSky telemetry and displays it on its screen that will work. For the extra data types that are not standard FrSky telemetry, I used the additional coding system described in part three. A Taranis doesn’t understand that data, of course, and just ignores it, – but it is still detects the standard GPS, variometer, and other data and displays that without a problem. To detect and display the additional data requires a custom Arduino decoder, as described in part three. I suppose it might be possible to create custom firmware for a Taranis to do the same, but I’ve not looked at that.

I was going to produce a cut-down version of my original code to post here, but I’m running short on time, so for those of you who wish to study it and extract the relevant parts to use in your own projects, I’m going to post my original ‘Piagma’ sketch. You can download the sketch using this link.

Piagma is a model plane autopilot I wrote that uses a Raspberry Pi for the higher-level functions of waypoint selection, steering, etc. An Arduino is used as an interface to read all the sensors, and as an interface for the Raspberry Pi. But you don’t need the Raspberry Pi for it to be useful – even without the Raspberry Pi, the Arduino reads all the sensors and sends the data back to the base station through the FrSky telemetry system. If a Raspberry Pi is present, the Arduino works as an SPI slave and the Raspberry Pi is the SPI master.

The sensors I used were a MPU6050 gyro/accelerometer, a BMP085 barometer/altimeter, a HMC5883L magnetometer, and a ‘standard’ serial data at 9600 baud GPS unit. The Arduino also decodes the CCPM data available from the FrSky receiver and drives the decoded data out to the individual servos and motor controls. One channel of CCPM data is used as an Auto/Manual switch – in the manual position the received channels from the base station are sent direct to the servos – in the auto position, the Raspberry Pi controls the servos. This means that when testing, if the autopilot goes crazy, or the Raspberry Pi crashes or stops communicating with the Arduino, then by flicking a switch on your transmitter you can take manual control.

When I get a bit more free time, I may produce a cut-down version of the Piagma Arduino sketch removing the Raspberry Pi parts to make it clearer. but maybe this post will help those of you who are keen to make progress.

I also wrote my own telemetry receiver / display / data logger for attaching to the ground station – that allows you to monitor all the telemetry data with an ordinary ‘dumb’ transmitter rather than a Taranis – that’s also Arduino based with a colour LCD screen and an SD card for data logging. If I ever get around to it, I guess I can cover that in part six or seven or whatever!

Decoding FrSKY telemetry data with an Arduino – Part three

So now we want to add our own types of data. We’ll still keep the standard FrSky telemetry data for all the standard sensors, but perhaps we want to send additional messages – these might be strings of characters, floating point values, integers, even possibly an image from a camera (though an image would take a long time to transmit due to the low bandwidth available).

To begin with I’ll concentrate on the modifications to the base station – remember that’s what I call the Arduino on the ground connected to the transmitter you’re holding. Once these modifications are done, the program will still work exactly the same as was described in part two, – but once we start injecting suitable data using an Arduino in our aircraft, this modified ‘base station’ program will be ready to also receive and display the new data packet types.

Recall that telemetry hub data frames were framed with the byte 0x5E and that we therefore had to do the byte stuffing technique to encode/decode 0x5E as 0x5D:0x3E and 0x5D as 0x5D:0x3D.

In the same way we’ll now use 0x6E to frame our own data frames so we’ll need to do a similar byte stuffing trick to encode/decode 0x6E as 0x6D:0x6C and 0x6D as 0x6D:0x6B. The bytes 0x5E and 0x5D might also occur inside our own data frames, and to avoid these being detected as ordinary NBDPs (see part two) within our own data frames we shall also encode 0x5E as 0x6D:0x6A and 0x5D as 0x6D:0x69.

This all sounds very complicated, but it doesn’t affect the existing code as the additional layer of byte stuffing only has to occur INSIDE our own data frames.

To keep it as simple as possible I’ll make all our own data frames the same length as each other: our frames will start and end with 0x6E. The first byte of our frame will be an identifier that tells us what variety of our data is being received, but regardless of that identifier, there will always be twenty bytes of data following before the ‘end-of-frame’ marking 0x6E. There’s no particular reason for choosing twenty, and no reason while all our data frames need be the same length – once you have the code working it will be easy to modify it to use different and/or variable lengths.

We’ll store the identifier inside our data packet in FrSkyTelemetryDataID, then receive 20 bytes and store them in myDataPacket, receive the terminating 0x6E, process whatever data is inside myDataPacket and then return to the normal processing loop described in part two. To start with, the processing of our own data packets will just consist of printing the identifier type, followed by the twenty bytes displayed as hex codes.

So here are the changes to the part two code (changes in orange) to do that. I’ve omitted lots of the existing code but included one or more lines of original code (in black) either side of the changes, so you can locate where the changes are:

void handleDataByte(uint8_t data) {
static uint8_t myDataPacket[21];
static uint8_t myDataIndex;
static uint8_t dataPacket[4];

switch (FrSkyUserDataMode) {
case 0: // idle
if (data == 0x5E) { // telemetry hub frames begin with ^ (0x5E)
FrSkyUserDataMode = 1; // expect telemetry hub DataID next
}
else if (data == 0x6E) { // myData frames begin with 0x6E
FrSkyUserDataMode = 3; // expect packet type next
}
break;
case 1: // expecting telemetry hub DataID

case 0x26: // z acceleration
Serial.print("Z acceleration: ");
Serial.print(format(FrSkyUserDataLow, data));
Serial.print("\n");
break;
default:
break;
}
FrSkyUserDataMode = 0; // received and stored both data bytes,
// so back to idle mode
} // else
break;
case 3: // expecting <packet type>
FrSkyUserDataStuffing = false;
FrSkyTelemetryDataID = data;
FrSkyUserDataMode = 4; // expect twenty bytes of data next
myDataIndex = 0;
break;
case 4: // expecting twenty bytes of data
if (FrSkyUserDataStuffing) {
FrSkyUserDataStuffing = false;
if (data == 0x6C)
data = 0x6E;
else if (data == 0x6B)
data = 0x6D;
else if (data == 0x6A)
data = 0x5E;
else if (data == 0x69)
data = 0x5D;
else { // byte stuffing is only valid for (unstuffed) bytes:
// 0x6E, 0x6D, 0x5E or 0x5D

FrSkyUserDataMode = 0; // back to idle mode
break;
}
}
else if (data == 0x6D) { // following byte should be the code
// for a stuffed byte

FrSkyUserDataStuffing = true;
break;
}
myDataPacket[myDataIndex++] = data;
if (myDataIndex >= 20) {
FrSkyUserDataMode = 5; // expect terminating 0x6E next
}
break;
case 5: // expecting terminating 0x6E of a myData data packet
if (data == 0x6E) {
switch (FrSkyTelemetryDataID) {
default:
Serial.print("myDataPacket: ");
if (FrSkyTelemetryDataID < 10) { // pad leading space
// for better formatting

Serial.print(" ");
}
Serial.print(FrSkyTelemetryDataID);
Serial.print(": ");
for (i = 0; i < 20; i++) {
if (myDataPacket[i] < 16) { // format all bytes
// as two hex digits

Serial.print("0");
}
Serial.print(myDataPacket[i], HEX);
Serial.print(" ");
}
Serial.print("\n");
break;
}
}
FrSkyUserDataMode = 0; // back to idle mode
break;
default: // should never happen
FrSkyUserDataMode = 0; // back to idle mode
break;
}
}

Here’s an example of what our modified program will display.

RSSI: 74 AD1: 71 AD2: 62
myDataPacket: 19: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
myDataPacket: 1: E6 0A D6 41 6A 60 EC BE 15 8D 39 42 00 00 00 00 00 00 00 00
myDataPacket: 2: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C8 41 01 00 01 00
RSSI: 77 AD1: 71 AD2: 62
myDataPacket: 3: 00 00 00 00 18 FC 00 00 00 00 00 00 07 00 78 00 64 03 00 00
myDataPacket: 4: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Barometric Altitude: 122.86
RSSI: 80 AD1: 71 AD2: 62
GPS C.O.G.: 00.00
GPS speed: 00.16
GPS Altitude: 79.06
Date: 2019-03-17
Time: 17:41:21
X acceleration: 08
Y acceleration: 455
Z acceleration: 901
Latitude: 53 deg 17.4902 min N
RSSI: 77 AD1: 71 AD2: 63
Longitude: 03 deg 33.1422 min W
Temperature 1: 27
Temperature 2: 28
myDataPacket: 17: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
RSSI: 78 AD1: 71 AD2: 62
myDataPacket: 18: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
myDataPacket: 19: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
myDataPacket: 1: 03 0F D6 41 FD CC EA BE AA E8 39 42 00 00 00 00 00 00 00 00
RSSI: 75 AD1: 71 AD2: 62

Note the new ‘myDataPacket’ lines interleaved with the previous data from part two.

Next we need to look at the code running on the aircraft that injects this data to the receiver, but this post is already getting long, so I’ll continue in part four

Solids of constant width – the Meissner Tetrahedron – in OpenSCAD

Lots of people know about curves of constant width – for example the UK 50p and 20p coins: despite their apparent non-roundness they have constant width when measured by parallel jawed calipers or the mechanisms inside vending machines.  If the shapes of these coins are extruded as prisms they can be used as rollers for moving heavy equipment around on hard floors.

The Reuleaux Triangle, pictured above is the best known curve of constant width (other than the circle, of course).  Notice in the animation above the triangle never quite reaches into the corners of the square, but it does remain in contact with all four sides of the square at all times.

In terms of three-dimensional solids the Reuleaux Tetrahedron is the analogue of the Reuleaux Triangle, but it is NOT a solid of constant width – it comes close but the edges stick out a bit too much: it’s about 2.5% wider across the midpoints of two edges than it is from a vertex to the opposite face.

You can produce solids of constant width fairly easily by rotating a curve of constant width – imagine spinning a 50p coin on its edge: providing the same point of the edge remains in contact with the surface it is spinning on, the volume swept out by the coin is a solid of constant width.  However these kind of solids are fairly boring because cross-sections perpendicular to the axis of rotation are just circles.

Much more interesting are the Meissner Tetrahedra.  In 1911 the Swiss mathematician, Ernst Meißner demonstrated how the Reuleaux Tetrahedron could be patched by rounding off three of its edges to produce true solids of constant width: there are two ways of doing it: one where the three rounded edges share a common vertex, and the other where the three rounded edges are around one face of the tetrahedron.

I set out to model the tetrahedra using the OpenSCAD CAD program, so that they could be printed on a 3D printer. OpenSCAD is free software available for Linux/UNIX, Windows and Mac OS X.  I recommend it.

So we start with the Reuleaux Tetrahedron.  It’s fairly easy to model as it’s formed by the intersection of four spheres with their centres located at the vertices of a regular tetrahedron where all the tetrahedron edges have the same length as the radius of the spheres.  As we’re making solids of constant width we name the radii of the spheres, ‘width’, and the edge lengths of the tetrahedron are also ‘width’.  This is a little disorienting at first, but you get used to it.

Wikipedia gives the Cartesian coordinates for the vertices of a tetrahedron falling on the unit sphere with the lower face level as:

v1 = ( sqrt(8/9), 0 , -1/3 )
v2 = ( -sqrt(2/9), sqrt(2/3), -1/3 )
v3 = ( -sqrt(2/9), -sqrt(2/3), -1/3 )
v4 = ( 0 , 0 , 1 )
with the edge length of sqrt(8/3).

Building that information into an OpenSCAD model and allowing for scaling the resulting Reuleaux Tetrahedron to any desired size gives the code:

// Reuleaux Tetrahedron
// ceptimus 2018-01-13

width = 50; // width of the object to be printed
$fn = 50; // higher numbers give smoother object but (much) longer render times

// vertices and edge length of Reuleaux Tetrahedron from Wikipedia
v1 = [sqrt(8/9), 0, -1/3];
v2 = [-sqrt(2/9), sqrt(2/3), -1/3];
v3 = [-sqrt(2/9), -sqrt(2/3), -1/3];
v4 = [0, 0, 1];
a = sqrt(8/3); // edge length given by above vertices

k = width / a; // scaling constant to generate desired width from standard vertices

reuleauxTetrahedron();

module reuleauxTetrahedron() {
    intersection() {
        translate(v1 * k)
            sphere(r = width);
        translate(v2 * k)
            sphere(r = width);
        translate(v3 * k)
            sphere(r = width);
        translate(v4 * k)
            rotate([90, 0, 0]) // use similar part of sphere for bottom face as other faces - OpenScad renders the parts of 'spheres' near the 'poles' differently
                sphere(r = width);
    }
}

…which results in this view:

If we alter the $fn parameter we can make it much smoother.  With $fn = 300; it still previews (F5) almost instantly but rendering (F6) takes a long time unless you have a very fast machine.

Now for the changes necessary to round off some edges to make it a true solid of constant width.  If we take one of the faces of the underlying tetrahedron and continue it out till it cuts one of the spherical faces of the Reuleaux Tetrahedron we get this two dimensional shape for the portion of that flat face between the triangle and the curved outer surface.

Here’s the code that produces that shape:

intersection() {
    // 6 is just an empirical 'big enough' figure so that the arc is contained within the rectangle
    translate([-width / 6, -sqrt(2/3) * k, 0])
        square([width / 6, width]);
    translate([(sqrt(2/9) + sqrt(8/9)) * k, 0, 0])
        circle(r = width);
}

And we can use OpenSCAD’s rotate_extrude() to spin that shape around and produce the three dimensional shape I call a ‘spindle’ – which is the shape we need to round off three of the Reuleaux Tetrahedron’s edges so as to make it constant width.

Now we need to lean the spindle over and line its two vertices up with two vertices of the tetrahedron to round off the edges, but first we need to remove some of the edge material of the tetrahedron that would otherwise project out beyond the spindle.  To do this we introduce the angle, alpha, which is half the angle formed by any two intersecting regular (not curved) tetrahedron faces.  This angle of a tetrahedron is called the ‘dihedral angle’ and alpha is half of that – about 35.2644 degrees.

My sketch makes a wedge shape with the dihedral angle (2 x alpha) for the front edge of the wedge and the wedge long enough to fit against one edge of the underlying tetrahedron and deep enough to ‘include’ all the material we need to cut away from one edge of the Reuleaux Tetrahedron before inserting the spindle.  Here’s what the tetrahedron looks like when the wedge is shown in its ‘cutting position’.

And here’s the code that does it:

alpha = atan(2 * sqrt(2)) / 2; // half 'dihedral angle' (amount the three edges up to the apex lean inwards from the vertical)

reuleauxTetrahedron();
color([1, 0, 0])
    translate(v1 * k)
        rotate([0, -alpha, 0])
            wedge();

module wedge() { // used to subtract three sharp edges of Reuleaux Tetrahedron prior to adding the rounded 'spindle' edges
    hull() {
        rotate([0, 0, -alpha])
            // 5 is just an empirical 'big enough' figure so that the wedge includes the Reuleaux Tetrahedron edge when placed appropriately
            cube([width/5, 0.001, width]);
        rotate([0, 0, alpha])
            translate([0, -0.001, 0])
                cube([width/5, 0.001, width]);        
    }
}

Now we can use OpenSCAD’s difference() function to subtract the wedge away from the Reuleaux Tetrahedron.

And then put the spindle in place to patch the edge.

Repeat that procedure three times and we’ve completed one of the two varieties of Meissner Tetrahedron.

Note that only three of the edges are rounded – the bottom (far) three edges are still sharp – if you were to round those off as well then the resulting solid WOULDN’T have constant width – the distance between opposite ‘edges’ would then be too small.

Here’s the complete code for the finished Meissner Tetrahedron.  Remember that you can increase the value of $fn to get a smoother rendering, but that although the preview (F5) will still work fast enough, the rendering (F6) can bring even a fast PC to its knees.

// Meissner Tetrahedron (solid of constant width) - common vertex variant
// ceptimus 2018-01-13

width = 50; // width of the object to be printed
$fn = 50; // higher numbers give smoother object but (much) longer render times

// vertices and edge length of Reuleaux Tetrahedron from Wikipedia
v1 = [sqrt(8/9), 0, -1/3];
v2 = [-sqrt(2/9), sqrt(2/3), -1/3];
v3 = [-sqrt(2/9), -sqrt(2/3), -1/3];
v4 = [0, 0, 1];
a = sqrt(8/3); // edge length given by above vertices

k = width / a; // scaling constant to generate desired width from standard vertices

alpha = atan(2 * sqrt(2)) / 2; // half 'dihedral angle' (amount the three edges up to the apex lean inwards from the vertical)

difference() {
    reuleauxTetrahedron();
    for (angle = [0 : 120 : 240])
        rotate([0, 0, angle])
            translate(v1 * k)
                rotate([0, -alpha, 0])
                    wedge();
}
color([1, 0, 0])
    for (angle = [0 : 120 : 240])
        rotate([0, 0, angle])
            translate(v1 * k)
                rotate([0, -alpha, 0])
                    spindle();

module reuleauxTetrahedron() {
    intersection() {
        translate(v1 * k)
            sphere(r = width);
        translate(v2 * k)
            sphere(r = width);
        translate(v3 * k)
            sphere(r = width);
        translate(v4 * k)
            rotate([90, 0, 0]) // use similar part of sphere for bottom face as other faces - OpenSCAD renders the parts of 'spheres' near the 'poles' differently
                sphere(r = width);
    }
}

module wedge() { // used to subtract three sharp edges of Reuleaux Tetrahedron prior to adding the rounded 'spindle' edges
    hull() {
        rotate([0, 0, -alpha])
            // 5 is just an empirical 'big enough' figure so that the wedge includes the Reuleaux Tetrahedron edge when placed appropriately
            cube([width/5, 0.001, width]);
        rotate([0, 0, alpha])
            translate([0, -0.001, 0])
                cube([width/5, 0.001, width]);        
    }
}

// rotate the curve produced where the Reuleaux Tetrahedron surface intersects with the (extended) underlying tetrahedron face
// this produces the correct spindle shape for rounding off three edges of the Reuleaux Tetrahedron to produce the Meissner Tetrahedron
module spindle() {
    translate([0, 0, width/2])
        rotate_extrude()
            intersection() {
                // 6 is just an empirical 'big enough' figure so that the arc is contained within the rectangle
                translate([-width / 6, -sqrt(2/3) * k, 0])
                    square([width / 6, width]);
                translate([(sqrt(2/9) + sqrt(8/9)) * k, 0, 0])
                    circle(r = width);
            }
}

Here’s the second variant where the three rounded edges are in a triangle rather than sharing a common vertex.

Here are the OpenSCAD files for the two variants.

http://ceptimus.co.uk/meissnerTetrahedronCommonVertex.scad

http://ceptimus.co.uk/meissnerTetrahedronTriangle.scad

E-paper display module driven by Arduino

These E-paper displays work well and look nice.  At the time of writing you can get the 1.54 – inch size (200 x 200 pixels) from Banggood, but there are other sizes in the same range and my code should work with the bigger ones too.  I have one on order to test.  Search on eBay for Waveshare E-paper module.  I’ve not tried the three colour (black, red, white) modules yet – those will need a tweak to the sketch.

Edit (January 8th) have now received the larger 4.2 – inch 400 x 300 pixel module and this WON’T work with my code without considerable modification.  The official Waveshare code doesn’t support partial refresh for this larger module at all.  There is a library by ZinggJM on GitHub that does support partial update but warns that it’s not official and could conceivably damage the display.  I’ll look at it soon…  (end of edit).

A naked AtMega 328 Arduino doesn’t really have enough RAM to drive these displays properly: even the 1.54 – inch display has a 5000 – byte display area and the display memory is write only.  The AtMega 328 only has 2k of RAM so it can’t contain a memory buffer for the full screen and this makes graphics difficult.  I overcame this problem by adding a serial RAM chip, 23LC1024, and using that as a paged memory display buffer.

The display is 3.3V device and 5V signals may kill it!  Best solution is to use a 3.3V Arduino.  You can get the Pro Mini ones that work at 3.3V (and should have an 8MHz crystal).  If you’re using a 5V Arduino, make sure to use some voltage level converters – you can get them in four or eight channel versions from Banggood or eBay.

Here’s the circuit.

I’ve attached the sketch.  To use the “driver” in your own sketches just copy the ePaper.h and ePaper.cpp files to the same folder as your .ino file.  Then you need a

#include “ePaper.h”

in your .ino file which then makes the following commands available:

ePaper::start();  Call from your setup() function before using any other ePaper commands

ePaper::clearBuffer(true);  Clears the display buffer to all white.  Use false to clear it to all black.

ePaper::displayFrame();  Actually refreshes the display so you can see it.  Normally you would do a bunch of other graphics operations (below) to create a display in the buffer before displaying it.

ePaper::text(x,  y,  “string”,  false);  Displays string with top left at (x, y) using black text on a white background.  Use true to display white text on black background.

ePaper::textSmall(x, y, “string”, false); Same as ::text but using a smaller font.

ePaper::pixel(x, y, false); Draws a black pixel at (x, y).  Use true for a white pixel.

ePaper::line(x1, y1, x2, y2, false); Draws a black straight line from (x1, y1) to (x2, y2).

ePaper::rectangle(x1, y1, x2, y2, false); Draws a black rectangle outline using vertical and horizontal lines where (x1, y1) and (x2, y2) are the coordinates of diagonally opposite corners.

ePaper::filledRectangle(x1, y1, x2, y2, false); Same as ::rectangle but the rectangle is solid black (or white if true is used) instead of just an outline.

ePaper::circle(x1, y1, radius, false); Draws a circle outline in black with specified radius centred at (x1, y1);

ePaper::filledCircle(x1, y1, radius, false); Same as ::circle, but filled.

ePaper::fullUpdateMode();  This sets the display so that ::displayFrame() does a complete update – this takes longer and causes the display to “flash”

ePaper::partialUpdateMode(); This sets the display so that ::displayFrame() works faster and without “flashing”.  The drawback is that with thousands of updates, perhaps spread over several days, the display isn’t as clear and may show greys in some areas rather than black or white.  You should arrange your sketch to switch to fullUpdateMode and then do a displayFrame once occasionally to prevent this from happening.  I normally do fullUpdateMode the first time when creating a fresh “page” and then use partialUpdateMode while updating/animating that page.

ePaper::sleep();  This puts the display into very low power mode where the display should not then disappear even if the power is turned off.  Recommended that your sketch does this if if doesn’t need to update the display for some time.

ePaper::wake();  Wakes from sleep.

Here’s the sketch: http://ceptimus.co.uk/ePaperDemo.zip

Revisiting the MinAttak chess problem

I blogged about my old Java Applet for the chess minimum attack (dominance) puzzle previously here.

But of course Java Applets are now deader than a very dead thing that’s also been poisoned, burnt, and crushed.  I’ve been playing around trying to learn a bit more JavaScript, and as a learning exercise, I coded up a MinAttak page that should work in any modern browser that has drag-and-drop capability.  Most phone and tablet browsers don’t have that yet – really you need a mouse.

Anyway it’s manual only at the moment, but I may add a solver to it if I don’t become too bored with JavaScript!  Try it out below – it’s not just an image – if you have a mouse you should be able to drag the pieces around and see it working.

Decoding FrSKY telemetry data with an Arduino – Part two

In part one, we looked at the standard FrSky ‘D series’ data packets that provide voltage and RSSI information.  In this part two post we’ll consider the data format used by standard FrSky telemetry add-ons and how to decode the data from those without using a Taranis transmitter or a commercial display unit.  In part three I’ll begin to explain how we can mimic that data format by injecting our own serial data from, say, an Arduino into the receiver’s serial port and then extend that idea to send and decode our own data packet types.

The terminology is confusing in that it’s the radio control “transmitter” that we usually hold in our hands is the receiver of telemetry data, and the radio control “receiver” in the aircraft (or other model) transmits the data.  To try to reduce the confusion a little, I shall refer to the base station and the aircraft.  If you’re using a surface vehicle such as a boat, just mentally substitute “boat” or whatever when you see “aircraft”.

Let’s consider what happens at the base station.  If you’ve read part one of this telemetry blog, you’ll remember that in our handlePacket() routine, we looked for packets that began with an 0xFE byte and ignored any other packet types.  The 0xFE packets contained the basic A1/A2/RSSI data.  Now all other data from all the standard telemetry sensors, plus your own injected serial data, arrive in a second type of packet that begins with an 0xFD character.  I shall call these second type of data packets, “non-basic-data-packets” or NBDPs for short.

Structure of a NBDP:

0xFD <length> <wasted> [up to six data bytes]

Following the 0xFD identifier is a <length> byte that indicates (in binary) the number of data bytes in the packet.  This should always have a value in the range one to six – if it’s outside that range then the packet is bad and can be ignored.  Next comes a wasted byte that contains no useful data.  Then follows the actual data – and the number of bytes here is the value that was received in <length>

So we can modify our existing handlePacket() routine so that it still decodes A1, A2, and RSSI as before, but also recognizes any NBDPs that are received and passes the data within them, a byte at a time, to another routine named handleDataByte()

void handlePacket(uint8_t *packet) {
  switch (packet[0]) {
    case 0xFD:
      if (packet[1] > 0 && packet[1] <= 6) {
        for (int i = 0; i < packet[1]; i++) {
          handleDataByte(packet[3 + i]);
        }
      }
      break;
    case 0xFE:
      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.
      break;
  }
  cli(); timeout = 1000; sei();
}

Now let’s consider the data stream format that the handleDataByte() routine has to decode.  It’s useful to study this FrSky document to understand what follows.

Data frames coming from the hub are framed (start and end) with 0x5E bytes.  As with the data at the (previously explained) lower level, there may be data bytes within the stream that happen to have the same value 0x5E so another layer of Byte stuffing is employed; inside the packets: 0x5E is replaced by the byte pair 0x5D, 0x3E, and then an actual data byte with value 0x5D also needs to be stuffed and this is replaced by the pair 0x5D, 0x3D.

Here’s the first part of the handleDataByte() routine.  It needs somewhere to store data as it arrives until it has a complete packet, and also has to remember what it’s doing between one call and the next and whether or not stuffing is in force so we have a few static variables:

void handleDataByte(uint8_t data) {
  static uint8_t dataPacket[4];
  static uint8_t FrSkyUserDataMode = 0;
  static uint8_t FrSkyUserDataLow;
  static uint8_t FrSkyTelemetryDataID;
  static bool FrSkyUserDataStuffing;
  static bool FrSkyUserDataLowFlag; // flag for low byte of data, which is the first of the following two
  int16_t i;
  uint8_t high, low;

  switch (FrSkyUserDataMode) {
    case 0: // idle
      if (data == 0x5E) { // telemetry hub frames begin with ^ (0x5E)
        FrSkyUserDataMode = 1; // expect telemetry hub DataID next
      }
      break;
    case 1: // expecting telemetry hub DataID
      if (data < 0x3C) { // store DataID (address) FrSkyUserDataStuffing = false; FrSkyTelemetryDataID = data; FrSkyUserDataMode = 2; // expect two bytes of data next FrSkyUserDataLowFlag = true; // flag for low byte of data, which is the first of the following two } else if (data != 0x5E) { // the header byte 0x5E may occur twice running as it is also used as an 'end of frame' so remain in mode 1. Otherwise DataID was > 0x3B, so invalid
        FrSkyUserDataMode = 0; // back to idle mode
      }
      break;
    case 2: // expecting two bytes of data
      if (FrSkyUserDataStuffing) {
        FrSkyUserDataStuffing = false;
        if ((data != 0x3D) && (data != 0x3E)) { // byte stuffing is only valid for (unstuffed) bytes 0x5D or or 0x5E
          FrSkyUserDataMode = 0; // back to idle mode
          break;
        }
        else {
          data ^= 0x20; // unstuff byte
        }
      }
      else if (data == 0x5D) { // following byte is stuffed
        FrSkyUserDataStuffing = true;
        break;
      }

That first part handles the the basics and the remainder of the routine is a big switch statement that stores data till complete packets are available and then displays the contents:

      if (FrSkyUserDataLowFlag) { // expecting low byte of data
        FrSkyUserDataLow = data; // remember low byte
        FrSkyUserDataLowFlag = false; // expect high byte next
      }
      else { // expecting high byte of data
        switch (FrSkyTelemetryDataID) {
          case 0x01: // 1st part of GPS altitude
          case 0x10: // 1st part of barimetric altitude
          case 0x11: // 1st part of GPS speed
          case 0x12: // 1st part of longitude
          case 0x13: // 1st part of latitude
          case 0x14: // 1st part of GPS course over ground
          case 0x15: // 1st part of date
          case 0x17: // 1st part of time
            dataPacket[0] = FrSkyUserDataLow;
            dataPacket[1] = data; // store value till last part received
            break;
          case 0x02: // temperature 1
            Serial.print("Temperature 1: ");
            Serial.print(format(FrSkyUserDataLow, data));
            Serial.print("\n");
            break;
          case 0x05: // temperature 2
            Serial.print("Temperature 2: ");
            Serial.print(format(FrSkyUserDataLow, data));
            Serial.print("\n");
            break;
          case 0x09: // second (final) part of GPS altitude
            Serial.print("GPS Altitude: ");
            Serial.print(format(dataPacket[0], dataPacket[1]));
            Serial.print(".");
            Serial.print(format(FrSkyUserDataLow, data));
            Serial.print("\n");
            break;
          case 0x16: // second (final) part of date
            Serial.print("Date: 20"); // assume 21st century
            Serial.print(format(FrSkyUserDataLow, 0)); // year
            Serial.print("-");
            Serial.print(format(dataPacket[1], 0)); // month
            Serial.print("-");
            Serial.print(format(dataPacket[0], 0)); // day
            Serial.print("\n");
            break;
          case 0x18: // second (final) part of time
            Serial.print("Time: ");
            Serial.print(format(dataPacket[0], 0)); // hour
            Serial.print(":");
            Serial.print(format(dataPacket[1], 0)); // minute
            Serial.print(":");
            Serial.print(format(FrSkyUserDataLow, 0)); // second
            Serial.print("\n");
            break;
          case 0x19: // second (final) part of GPS speed
            Serial.print("GPS speed: ");
            Serial.print(format(dataPacket[0], dataPacket[1]));
            Serial.print(".");
            Serial.print(format(FrSkyUserDataLow, data));
            Serial.print("\n");
            break;
          case 0x1A: // second part of longitude
          case 0x1B: // second part of latitude
            dataPacket[2] = FrSkyUserDataLow;
            dataPacket[3] = data; // store value till last part received
            break;
          case 0x1C: // second (final) part of GPS cog
            Serial.print("GPS C.O.G.: ");
            Serial.print(format(dataPacket[0], dataPacket[1]));
            Serial.print(".");
            Serial.print(format(FrSkyUserDataLow, data));
            Serial.print("\n");
            break;
          case 0x21: // second (final) part of barometric altitude
            Serial.print("Barometric Altitude: ");
            Serial.print(format(dataPacket[0], dataPacket[1]));
            Serial.print(".");
            Serial.print(format(FrSkyUserDataLow, data));
            Serial.print("\n");
            break;
          case 0x22: // third (final) part of longitude
            Serial.print("Longitude: ");
            i = (uint16_t)dataPacket[1];
            i = (i << 8) | dataPacket[0]; // degrees * 100 + minutes high = (i / 100) >> 8; low = (i / 100) & 0x00FF;
            Serial.print(format(low, high));
            Serial.print(" deg ");
            Serial.print(format(i % 100, 0));
            Serial.print(".");
            Serial.print(format(dataPacket[2], dataPacket[3]));
            Serial.print(" min ");
            Serial.write(FrSkyUserDataLow); // 'E' or 'W'
            Serial.print("\n");
            break;
          case 0x23: // third (final) part of latitude
            Serial.print("Latitude: ");
            i = (uint16_t)dataPacket[1];
            i = (i << 8) | dataPacket[0]; // degrees * 100 + minutes high = (i / 100) >> 8; low = (i / 100) & 0x00FF;
            Serial.print(format(low, high));
            Serial.print(" deg ");
            Serial.print(format(i % 100, 0));
            Serial.print(".");
            Serial.print(format(dataPacket[2], dataPacket[3]));
            Serial.print(" min ");
            Serial.write(FrSkyUserDataLow); // 'N' or 'S'
            Serial.print("\n");
            break;
          case 0x24: // x acceleration
            Serial.print("X acceleration: ");
            Serial.print(format(FrSkyUserDataLow, data));
            Serial.print("\n");
            break;
          case 0x25: // y acceleration
            Serial.print("Y acceleration: ");
            Serial.print(format(FrSkyUserDataLow, data));
            Serial.print("\n");
            break;
          case 0x26: // z acceleration
            Serial.print("Z acceleration: ");
            Serial.print(format(FrSkyUserDataLow, data));
            Serial.print("\n");
            break;
          default:
            break;
        }
        FrSkyUserDataMode = 0; // received and stored both data bytes, so back to idle mode
      } // else
      break;
    default: // should never happen
      FrSkyUserDataMode = 0; // back to idle mode
      break;
  }
}

The above routine makes calls to format() which takes two bytes of data representing a 16-bit value and formats the output as an ASCII decimal number.

char* format(uint8_t low, uint8_t high) { // format the 16-bit integer formed by high:low in ASCII decimal.  Negative values have a leading '-' values < 10 have one leading '0'
  static char buffer[8];
  char *p;
  p = buffer;
  int16_t i = (int16_t)high;
  i = (i << 8) | low;
  if (i < 0) {
    *p++ = '-';
    i = -i;
  }
  if (i < 9) { *p++ = '0'; } for (int16_t tenPower = i > 9999 ? 10000 : i > 999 ? 1000 : i > 99 ? 100 : i > 9 ? 10 : 1; tenPower > 0; tenPower /= 10) {
    uint8_t digit = '0';
    while (i >= tenPower) {
      digit++;
      i -= tenPower;
    }
    *p++ = digit;
  }
  *p = '\0';
  return buffer;
}

Here’s some sample output.  I changed the latitude and longitude values so as not to show my home location:

RSSI: 87 AD1: 96 AD2: 142
RSSI: 87 AD1: 96 AD2: 141
Barometric Altitude: 95.18
GPS C.O.G.: 00.00
GPS speed: 00.96
RSSI: 87 AD1: 96 AD2: 143
GPS Altitude: 63.00
Date: 2017-10-10
Time: 20:01:27
X acceleration: -60
Y acceleration: 133
Z acceleration: 963
Latitude: 53 deg 17.4902 min N
Longitude: 03 deg 33.1422 min W
RSSI: 87 AD1: 96 AD2: 141
Temperature 1: 25
Temperature 2: 26
RSSI: 86 AD1: 96 AD2: 143
RSSI: 87 AD1: 96 AD2: 140

Note that the RSSI frames arrive more frequently than the other types of data.

You can download the complete working sketch using this link.

Continued in part three…

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.

Part 2

POV source code – part 3

If you’re trying to compile the source code with the Keil compiler, you’re probably getting error messages about ‘undefined identifier’ or similar.  This is because the standard Keil reg52.h header file doesn’t define all the necessary identifiers for the STC89C5x chips.

We need to define the special function register (sfr)

P4 = 0xE8;

so the code can access bits of the fourth GPIO port and then define the special bit (sbit)

INT2 = P4^3;

so that the code can react to the infra red photodiode that is connected to that pin.  It seems that the best way to include these extra hardware definitions is to edit and save the standard reg52.h file.  Here’s my modified version: reg52.h

Timer 2 interrupt

The bottom line of the display is handled in a slightly different way.  The idea is that the current rotation rate (measured inside the timer 1 interrupt) is used to calculate the settings for timer 2 so that the timer2 interrupt occurs 256 times per revolution.  Then from inside the timer2 interrupt code we just have to output the next set of 8 pixels to each of the ports that control the lower set of 8 LEDS on the arms.

Timer 2 has a 16-bit counter and an interrupt is generated when it counts up to FFFF.  To get 256 interrupts per turn, we count the number of ticks per turn from interrupt 1 and work out 3/4 of that value.  This is subtracted from FFFF to get the ‘start’ count.

Inside the timer 2 interrupt we count how many interrupts actually occur and use this to tweak the timer 2 start count slightly so that the 256 pulses eventually synchronize and stay in register with the infra red photodiode pulse.  If 256 or more pulses occurred in the last revolution the start value is tweaked slightly lower so that the interrupts during the next revolution happen slightly slower – and vice versa if 255 or fewer pulses occurred.

With the comments in the code that should enable you to modify the code for your own applications.  Be aware that the free version of the Keil compiler limits the compiled code size (to 2K, I think) so the full program here won’t produce a hex file.  But if you trim the program down to only work the clock on the upper row or only scroll a limited amount of text on the lower row, then you can get it to fit within 2K.

For an individual just writing code for fun, the Keil licence costs way too much: if you want to write larger programs you have to use one of the free alternatives to the Keil compiler – which are a little more tricky to set up and get working.