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 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.

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.

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.

POV source code – part 2

Clock interrupt

The STC89C52 has three timer/counters (most of the 8051 chips have the first two, and the third one is pretty common).  One of the modes for timer 1 is to count clock pulses in an 8-bit register and when FF is reached, reload the count register from a separate 8-bit register.  Each time this overflow is reached an interrupt is generated, and we can use this to provide a regular clock ‘tick’.

The crystal frequency is divided by 6 when the chip is set to fast mode and by 12 when it’s set to standard (slow) mode.  The crystal oscillates at 11.0592 MHz so assuming we’re using fast mode the slowest rate we can generate interrupts this way would be to set the timer reload value to zero and then we’d get:

(11059200 / 6) / 256 interrupts per second = 7200 Hz.

I wanted the interrupts to happen a little faster than that as I was also intending to piggy-back the LED switching from the interrupt routine, so I chose a reload value of 0x40 (64) which results in 9600 interrupts per second.

The set-up code to initialize the timer is as follows:

// set up timer 1 interrupt for 9.6 kHz (assuming 11.0592 MHz xtal)
TMOD = 0x20; // timer 1 in 8-bit auto-reload mode
TH1 = 0x40; // reload value to give 9.6 kHz in ‘fast’ (6T) mode
ET1 = 1; // enable timer 1 interrupt
EA = 1; // enable interrupts
TR1 = 1; // run timer 1

The first part of the interrupt handler just counts the interrupts and uses them to advance the clock: (seconds, minutes, hours)

unsigned int fracSecs; // fractions of second in ticks

void T1INT(void) interrupt 3 using 1 { // configured to fire @ 9.6 kHz
    if (++fracSecs == 9600) { // one second elapsed
        fracSecs = 0;
        if (++topRow[7] == 26) { // units seconds overflow
            topRow[7] = 16;
            if (++topRow[6] == 22) { // tens seconds overflow
                topRow[6] = 16;
                if (++topRow[4] == 26) { // units minutes overflow
                    topRow[4] = 16;
                    if (++topRow[3] == 22) { // tens minutes overflow 
                        topRow[3] = 16;
                        // check for midnight
                        if (++topRow[1] == 20 && topRow[0] == 18) {
                            topRow[0] = topRow[1] = 16;
                        } else if (topRow[1] == 26) { // 10am or 8pm
                            topRow[1] = 16;
                            ++topRow[0];
                        }
                    }
                }
            }
        }
    }
}

It’s a little confusing as the routine directly manipulates ‘topRow’ which is a 16-character buffer holding the data to display on the top row of the display in the format: “HH:MM:SS        “ except that the character values are not ASCII but (ASCII – 32) to suit the way the font is defined.

So the first check (topRow[7] == 26) checks if the units seconds character has reached ASCII 58 (a colon – which is the character that comes after ‘9’) and if so it resets it back to 16 (ASCII 48 to display a ‘0’) and increments topRow[6] which is the tens of seconds character, and so on…

So why did I choose 9.6kHz?  Well it was a compromise to do with the rate that the LEDs spin round at, and wanting to get a reasonably well-defined display only using a single interrupt source and without overloading the microcontroller with too many interrupts.  Actually when I moved onto the bottom row display, I used a second interrupt source auto-adjusted to give exactly 256 interrupts per rotor revolution – but more on that later…

When powered with a nominal five volts, the board spins about 900 rpm (revolutions per minute).  The lowest voltage my kit would operate at without the microcontroller ‘browning out’ and resetting was about 3.3V which gave 700 rpm.  It’s unreliable at this low speed – if you try to switch too many LEDs on at the same time the extra current drags down the voltage on the board to the point where the microcontroller resets itself.  The ‘regulator’ on the board is a little sketchy – just a zener diode to bleed away current when the voltage begins to exceed 5.1V – so I’ve not risked going above 6V on the power supply at which voltage the motor still only spins about 950 rpm.

My program displays 32 characters around the circumference of the cylinder drawn out by the LEDs and with 8 pixel width characters at 900 rpm that gives a pixel rate of 900/60 x 32 x 8 = 3840 pixels per second.  As the voltage varies or the rotor is affected by draughts or similar, the rate can vary in the range roughly 3000 to 4000 pixels per second.

So at an interrupt rate of 9.6kHz we need to output a fresh set of pixels roughly every two-and-a-half interrupts (9600 / 3840).  We can’t do half-interrupts, of course, so the program needs to use a mixture of two, three, maybe even four, interrupts between each update – and spread them out in such a way as to provide a fairly regular and stable display.

As the rotor passes a reference position each revolution, it picks up an ‘index pulse’ from a photodiode illuminated by a stationary infra red LED – so it can use that information to recalculate the correct ratio of two interrupt/three interrupt cycles to keep the display fairly steady.

It sounds like a lot of calculation, maybe involving division (which is very slow on this chip as it doesn’t have multiply or divide instructions so it has to ‘long division’ by repeated subtraction/addition and bit-shifts).  Luckily there is a faster way just involving addition and bit shifts, although it’s a little confusing until you’ve studied it for a while.

The basic idea is to have a 32-bit counter which we try to get to count up to 0x01000000 (16777216) for each rev of the rotor.  We do this by adding an amount to the counter each time there is an interrupt.  If the rotor speed were exactly 900 rpm (fifteen revs per second) then we’re going to get 9600 / 15 interrupts  (640) for each revolution of the rotor so to count up to our target number we add 0x01000000 / 640 (which is about 26214)  every interrupt.

Now when the index pulse comes around we can see if we’ve counted past our target number or not reached it – and the error between the actual count and the target can be used to adjust our 26214 value up or down for the next revolution.  If you just divide the error by 1024 and add that value to the ‘26214’ then the counter quickly locks into counting to the correct target value after a few revolutions – and then it automatically adjusts the rate to compensate for any changes in the rotor speed.  Dividing by 1024 is something the microcontroller can do pretty quickly – just by bit shifts.  In fact because the 32-bit count is held in four 8-bit registers, the correction calculation can be done with just  a 2-bit shift on a 16-bit quantity and a 16-bit subtraction.

Looking at the 32-bit count as four bytes, we see that the most significant byte is targeted to count up to exactly ‘1’ per revolution, which means that the next most significant byte counts from 0 to 255 per revolution with the 0 aligned with the index pulse – so we can just look at this byte to give us the current position around the circle in “brads” (one brad = 1/256 of a circle or 1.40625 degrees).

That should be enough explanation to allow you to understand the source code which you can see/download: here.

I’ve not covered how the second interrupt source for the lower row of text works yet though – that’s for another post…

 

 

POV source code – part 1

These posts relate to the previous few concerning the Banggood kit: Cross LED Dot Matrix Display Circuit Board Rotating Electronic Kit

It uses an STC89C52RC microcontroller which is a (fairly) modern Chinese version in the 8051 family.

You can program it in assembler, but I chose to use the C language for this project.

The font

We’re going to be displaying text so we need some kind of font.  The kit provides 16 LEDs arranged vertically on each of the two arms, but I thought that if we use characters the full height of the arms then we wouldn’t be able to fit many of these large characters around the cylinder that the arms sweep out (unless the characters were ridiculously skinny for their height) so I decided to use an 8×8 font which would allow for two rows of characters.

For simplicity I wanted the characters defined in the font to include the spacing to the left and/or right of normal characters so most characters in the font are only 7 pixels wide – or even less for skinny characters such as i – and only a few characters like q and y have descenders, so the spacing between the two rows of characters is included in the font too.

The kit sensibly arranges the pins driving the LEDs so that the chip’s four 8-bit I/O ports, P0, P1, P2, P3 each drive either the top 8 LEDS or the bottom 8 LEDs on an arm:  one arm has P2 on the top and P0 on the bottom, the other arm has P1 at the top and P3 at the bottom (at least that’s the way mine turned out).  I chose to fit all blue LEDs to the P0/P2 arm and all red LEDs to the other one – obviously you can fit them in other ways – it might look pretty to get two extra colours – then you might have (say) yellow and green on the top row with red and blue on the bottom.

Anyway, each of the four ports has the least significant bit at the top, and then the bits going in order down to the most significant bit at the bottom.  If you wire up the motor with the red wire as positive, the board spins clockwise (viewed from above) so the LEDs scan the characters out in right-to-left order.  The wiring is such that the program has to write a ‘0’ to an I/O pin to illuminate the corresponding pin, or write a ‘1’ to switch it off.  It’s convenient to write 8-bit quantities in hexadecimal and in the C language we write 0xFF to indicate all 8 bits high or 0x00 for all eight bits low.

I searched on line and found this 8×8 font which I thought would be suitable:

toncFont The font is on this page and includes the data in C-friendly form http://www.coranac.com/tonc/text/text.htm

I had to manipulate the font data for right-to-left scan order with the least significant bit at the top and a zero-bit indicating ‘LED on’.  I wrote a C# program to do that.

Note that the font starts with the space character (ASCII 32) and is in ASCII order.  The last character (ASCII 127 [DEL]) renders a space too, but as that’s a duplicate you could tweak it to get a £ or € or some other symbol you might want to display.

Say our program wants to display the number 2  This is the ASCII character 50.  Our font doesn’t have characters defined for the first 32 ASCII values so we subtract 32 to get 18 and then multiply by 8 (eight bytes per character in the table) to get 144.  The data we need to display a 2 therefore starts 144 bytes into the font table and consists of the eight bytes:

0xFF,0xB9,0xB0,0xA6,0x8E,0x9C,0xBD,0xFF

So the program outputs those eight bytes in that order and (remembering that the arms scan out the characters from right to left) we get:

two

Cheap POV (persistence of vision) clock kit

POVclockThese kits are available on Banggood.com for about $11 shipping included.  Tricky soldering, and they don’t come with any instructions.  There are instructions available on the website – but not unless you read Chinese.  The pictures on the website help though.

Anyway, I recommend the kit.  Even if you can’t get it working properly, it’s worth the money for the parts alone – you get everything you need apart from a bit of strip board to solder the power supply components to, and a hunk of aluminium to act as a heat sink on one power transistor.

It runs on about 4 volts up to 5 or so.  I wouldn’t recommend going much above 5.5 volts as the ‘regulator’ on the main board is just a small zener diode that leaks current away once the voltage gets up to 5.1  It draws about a third of an amp – I run mine off a single LiPo cell.

Gary asked about the inside of the box.  Please see VoltLog’s build video – the only thing I did differently was to build up the circuit on some stripboard instead of in mid-air, and I added a bit of aluminium plate to the main power transistor to act as a heatsink.

There are more (and bigger) photos of the top and bottom of my stripboard layout here:  http://ceptimus.co.uk/pov/

Look at this video on the Voltlog YouTube channel to get an idea of how tricky it is to build.

I certainly got good value from it as it also comes without a program – so I wrote one, and no tool for uploading the program to the microcontroller.  See my previous post for the upload tool.  Here’s the link to the hex file for my clock program. (Right click and save as… if it opens up as a text file when you click on the link).

http://ceptimus.co.uk/POVclock.hex

I made a quick video showing how to set the time.  The camera settings were bad though – auto focus and strobing problems. 🙁

ISP programmer for the STC89C51 and STC89C52

I couldn’t find an ISP (programmer) for the STC89C52RC that worked with modern versions of Windows. I found several programmers that work with other chips in the STC range.

I did find one that worked on Windows XP, and with some hacking got it to work with Windows 7; however I had no luck with Windows 10. In any case, it was a Chinese program, and as I don’t read that language, I just had to remember which controls to use.

I also found one that worked under Linux, from the command line.  Using that source code as a guide I figured out how to write my own Windows version.

Image1It’s pretty basic but all you really need.  You select the COM port and the hex file you want to upload.  Connect up to your chip with a 5-volt serial port such as an FTDI (or you can use an Arduino UNO or similar that has a 5V FTDI built-in).

Here’s the video showing how to use the Arduino UNO as a programmer.  Here it’s being used to program a Banggood POV kit which uses an STC89C52RC chip – but you could obviously adapt the Arduino sketch and/or make a wiring harness to work with any STC89 board:

 

Here’s the Arduino sketch when you’re using the UNO as a programmer as shown in the video: Arduino sketch

Edit: the programmer program is now at V0.4 which fixes a few bugs and lets you switch the microcontroller to high speed (6T per machine cycle) or normal speed (12T per machine cycle).

Further Edit (August 2017): the programmer program is now at V0.5, and supports some different STC-chip bootloader firmware versions that previous versions did not.

If you’re not using the Arduino (which powers up the STC chip by itself when it detects activity on the serial port) then press the button to start the upload and then apply power to the target chip.  There’s a progress bar indicator – it takes maybe ten seconds to program the chip.

I’ve tried it on Windows 10 and Windows 7.  I’ve no reason to suspect that it won’t work with other versions of Windows.

Beware the fake Prolific serial chips that come in some USB-serial adapters and some cheap STC89C5x development boards.  I have one of those boards. I found an old version of the driver that let me get the Prolific chip operating as a loop back device – it even worked under Windows 10.  But it’s flaky and doesn’t work for programming the chip.  I found the only way I could get the programmer to work reliably was to cut the tracks leading from the STC chip’s serial port to the Prolific chip and connect an FTDI interface to the STC chip instead.

You may also need to fit a pull-up resistor to the STC’s TxD pin – only try that if you think you’ve connected everything up properly but it’s still not working – something like 10K or 100K should do the trick.  It depends on how much pull-up effect is already present on the RxD line of your serial interface and on the amount of stray capacitance present on your boards and cables.

Here is the link to download the install zip file for the programmer

Update: Ewald Burger (see comments below) encountered problems with the installation that he eventually traced to his AVAST virus scanner.  If you encounter problems with the installation you may want to try temporarily disabling your virus scanner while installing.

Here’s the source code – unzip it to your ‘projects’ folder. It was done using the free version of Visual Studio 2015 source code

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.