Radio Telemetry – Connecting it all together (5)

(I try to link to the products I used so you can find them more easily. If you purchase them from these links I may receive compensation from affiliate programs. I am not employed or influenced by the manufacturers or distributors.)

Now that we know we will be using the Arduino Nano 33 BLE Sense and (for the sake of demonstration) the HC-12 RF module, let’s go ahead and put the software and hardware together. I’ll try to go into as much detail here as possible, and cover many of the issues I had, but there’s a lot, so bear with me. For all of the schematics I use Fritzing and I currently still write all of my code in the free Arduino IDE.

Sending sensor data over serial

First we can start with just reading as many of the sensors as we’d like to from the Arduino Nano and writing their values to the USB connection’s serial port. I load this particular sketch quite frequently when I’m checking the functionality of the board or the computer’s telemetry software (which we will cover later). This isn’t some beautiful perfectly written code, but it works just fine.

// Start with the includes
#include <Arduino_LSM9DS1.h>
#include <Arduino_LPS22HB.h>
#include <Arduino_HTS221.h>
#include <Arduino_APDS9960.h>

// These variables will be used to store the sensor values on each cycle
float ax;
float ay;
float az;
float cx;
float cy;
float cz;
float gx;
float gy;
float gz;
float pressure;
float temperature;
float humidity;
float prox;

unsigned long timer = millis();                 // Delay Timer

const int redPin = 22;
const int greenPin = 23;
const int bluePin = 24;
boolean flash = false;
const int statusColor = redPin;
boolean err = false;


void setup() {
  pinMode(22, OUTPUT);
  pinMode(23, OUTPUT);
  pinMode(24, OUTPUT);
  digitalWrite(redPin, LOW); // Red on while we initialize
  delay(500);
  Serial.begin(9600);

  // These conditional statements give me feedback on Serial and with the LED
  if (!IMU.begin()) {
    Serial.println("Error initializing IMU sensor!");
    err = true;
  }
  if (!BARO.begin()) {
    Serial.println("Error initializing BARO sensor!");
    err = true;
  }
  if (!HTS.begin()) {
    Serial.println("Error initializing HTS sensor!");
    err = true;
  }
  if (!APDS.begin()) {
    Serial.println("Error initializing APDS sensor!");
    err = true;
  }
  digitalWrite(greenPin, LOW);
  if (!err) {
    digitalWrite(redPin, HIGH);
  }
}

void loop() {
  flash = !flash; // Alternating the LED is useful for visually checking cycle time
  digitalWrite(greenPin, flash); // green
  if (err) {
    digitalWrite(redPin, flash);
  }

  // Now we will read all of the sensors and write their values to the variables from before
  IMU.readAcceleration(ax, ay, az);
  IMU.readMagneticField(cx, cy, cz);
  IMU.readGyroscope(gx, gy, gz);
  pressure = BARO.readPressure();
  temperature = HTS.readTemperature();
  humidity = HTS.readHumidity();

  // Now we will print out a line of text over serial with each valuee separated by spaces
  Serial.print(millis());
  Serial.print(" ");
  Serial.print(pressure);
  Serial.print(" ");
  Serial.print(temperature);
  Serial.print(" ");
  Serial.print(humidity);
  Serial.print(" ");
  Serial.print(ax);
  Serial.print(" ");
  Serial.print(ay);
  Serial.print(" ");
  Serial.print(az);
  Serial.print(" ");
  Serial.print(cx);
  Serial.print(" ");
  Serial.print(cy);
  Serial.print(" ");
  Serial.print(cz);
  Serial.print(" ");
  Serial.print(gx);
  Serial.print(" ");
  Serial.print(gy);
  Serial.print(" ");
  Serial.println(gz);
}

If you want to see the output from this, while you’re in the Arduino editor, you can select the appropriate port in Tools > Port, and then view the output with Tools > Serial Monitor or as a graph in Tools > Serial plotter. Keep in mind that serial plotter will automatically pick up on the available values, plot them, and resize the screen to fit them. Because of this, the millis() function that I am using to track the delta-time will cause the screen to resize rapidly as the value increases, relegating the other values to appearing like straight lines with minuscule deviations. If you want a better plot then just comment out the Serial.print(millis()); line.

Your output will come out looking something like this if you’re really agitating the IMU. You can do a moving average if you’d rather have smoother curves (like in this picture) and you can even apply some kind of coefficient to normalize the values (in this example the denominator used in the moving average calculation).

If you’re not able to see the output from the Serial.print() statements, make sure that the “baud” in the bottom left of Serial Monitor or Serial Plotter matches the number from the Serial.begin() line.

Serial Plotter showing IMU data from the Arduino

I was back and forth about normalizing the IMU data, but I ultimately decided against it for a couple of reasons. First, if you are trying to make decisions based on sensor data, there’s no reason to water down the readings. I will eventually need to add some logic in to make decisions based on values over time, etc., but normalization doesn’t solve this problem.

The second reason is because while normalization does smooth out the graph beautifully, it also makes the readings lag behind by a significant amount. Timeliness matters when making automated in-flight decisions, so it’s better to let the flight computer have the raw values and then figure out how to handle them on its own.

Making the Transmitter

When it comes to wiring the HC-12 to the Arduino that we will be using as the telemetry computer, it’s pretty straightforward. I know that there are some great things you can do to make the circuits safer and more reliable, but I already told you this is not the place for that. You can easily connect the two devices pin-for-pin and it will work just fine. Here’s a handy chart of the pin names for your reference moving forward:

The Arduino has two different serial channels on it, which we will take advantage of here. There is a separate Serial1 channel with its own hardware TX and RX pins. Since radios use serial communications, we can use these to connect to the HC-12, which really simplifies the configuration.

Wireless telemetry transmitter wiring diagram

First, you will want a 9v battery connector. This can be accomplished by connecting the negative terminal to the ground, and the positive terminal to the VIN pin. You can power the Arduino in various ways, but some of them will only result in the 3V3 power output working. Using a 9V battery on the VIN will power the 5V power output, which is the voltage you need for optimum performance of the HC-12.

Next, to connect the HC-12, there are five wires that connect to the Arduino:

  1. The first two are the GND (black) and VCC (red), which make a complete circuit so electricity can flow between the two devices, powering the HC-12.
  2. Then connect the SET pin (Orange) on the HC-12 to digital out #2 on the Arduino. This pin will be defined in the software, and will be used to put the HC-12 into configuration mode so we can set channel number, TX strength, etc. We won’t use this pin, but it’s good to wire it up in case we need it.
  3. The TXD pin on the HC-12 connects to the RX pin on the Arduino (green) and the RXD pin on the HC-12 connects to the TX pin on the Arduino (Yellow). The reason for the flip-flop is because the labelling refers to the function in the context of the local device. You want the data to flow from a transmitting pin to a receiving pin, otherwise it wouldn’t be read.

Finally we connect the antenna. The HC-12 comes with small coil antennas that work just fine for shorter range applications, but you can pick up a couple of high gain antennas for $12 that will greatly improve your signal strength and quality.

3dbi high-gain antennas from Amazon

Programming the Transmitter

Now that the transmitting device is connected, we need to update our Arduino sketch from before with the code needed to operate the radio. Remember when I said before that the Nano has a built-in hardware Serial1. Technically it’s as simple as changing the Serial.print() statements to Serial1.print(). I’ll show you some more features of the HC-12 in a moment, but here’s a functioning copy of the remote telemetry code:

// Start with the includes
#include <Arduino_LSM9DS1.h>
#include <Arduino_LPS22HB.h>
#include <Arduino_HTS221.h>
#include <Arduino_APDS9960.h>

// These variables will be used to store the sensor values on each cycle
float ax;
float ay;
float az;
float cx;
float cy;
float cz;
float gx;
float gy;
float gz;
float pressure;
float temperature;
float humidity;
float prox;

unsigned long timer = millis();                 // Delay Timer

const int redPin = 22;
const int greenPin = 23;
const int bluePin = 24;
boolean flash = false;
const int statusColor = redPin;
boolean err = false;


void setup() {
  pinMode(22, OUTPUT);
  pinMode(23, OUTPUT);
  pinMode(24, OUTPUT);
  digitalWrite(redPin, LOW); // Red on while we initialize
  delay(500);
  Serial1.begin(9600);

  // These conditional statements give me feedback on Serial1 and with the LED
  if (!IMU.begin()) {
    Serial1.println("Error initializing IMU sensor!");
    err = true;
  }
  if (!BARO.begin()) {
    Serial1.println("Error initializing BARO sensor!");
    err = true;
  }
  if (!HTS.begin()) {
    Serial1.println("Error initializing HTS sensor!");
    err = true;
  }
  if (!APDS.begin()) {
    Serial1.println("Error initializing APDS sensor!");
    err = true;
  }
  digitalWrite(greenPin, LOW);
  if (!err) {
    digitalWrite(redPin, HIGH);
  }
}

void loop() {
  flash = !flash; // Alternating the LED is useful for visually checking cycle time
  digitalWrite(greenPin, flash); // green
  if (err) {
    digitalWrite(redPin, flash);
  }

  // Now we will read all of the sensors and write their values to the variables from before
  IMU.readAcceleration(ax, ay, az);
  IMU.readMagneticField(cx, cy, cz);
  IMU.readGyroscope(gx, gy, gz);
  pressure = BARO.readPressure();
  temperature = HTS.readTemperature();
  humidity = HTS.readHumidity();

  // Now we will print out a line of text over serial with each value separated by spaces
  Serial1.print(millis());
  Serial1.print(" ");
  Serial1.print(pressure);
  Serial1.print(" ");
  Serial1.print(temperature);
  Serial1.print(" ");
  Serial1.print(humidity);
  Serial1.print(" ");
  Serial1.print(ax);
  Serial1.print(" ");
  Serial1.print(ay);
  Serial1.print(" ");
  Serial1.print(az);
  Serial1.print(" ");
  Serial1.print(cx);
  Serial1.print(" ");
  Serial1.print(cy);
  Serial1.print(" ");
  Serial1.print(cz);
  Serial1.print(" ");
  Serial1.print(gx);
  Serial1.print(" ");
  Serial1.print(gy);
  Serial1.print(" ");
  Serial1.println(gz);
}

When I was experimenting I noticed that the status light was flashing slower and slower with each new feature I was adding. This was of course to be expected because each line of code causes the loop to take longer to process, but I was not anticipating it to be so noticeable. After some experimentation I found that the biggest culprit was reading the humidity sensor. Since this was not essential for telemetry, and was more for curiosity, I eventually removed the humidity sensor from the equation.

One huge realization I had during this process was that since the Arduino could not handle parallel tasks, if I want to use the Arduino as the flight controller I’m going to have to really strip the components down to the essentials. If I still want to monitor

Making the Receiver

Now it’s time to wire up the second Arduino (the Uno) so it can act as the data receiver, and relay the data to the computer over the USB connection. There are some pre-built 433MHz serial data receivers, but where’s the fun in that??

Wiring diagram for the Arduino Uno and HC-12 telemetry receiver

Since we will be using the USB connection from the computer, this one is much simpler, but we will also be limited in the amount of voltage we can supply. Using the 3.3V output that we are limited to from the computer won’t be as much of a problem since we won’t be transmitting from this device, just receiving. To connect the HC-12 is just like before with one minor variation for the serial ports.

  1. The first two are the GND (black) and VCC (red), which make a complete circuit so electricity can flow between the two devices, powering the HC-12.
  2. Then connect the SET pin (Orange) on the HC-12 to pin #4 on the Arduino. This pin will be defined in the software, and will be used to put the HC-12 into configuration mode so we can set channel number, TX strength, etc. Since this device will be connected to the computer, we will probably use the SET pin at some point.
  3. This time we will do something completely different with the TX and RX pins. The Arduino Uno has a library called Software Serial that will allow us to define a serial channel without having to use the onboard pins. Because of this, the TXD pin on the HC-12 connects to pin 5 on the Arduino (green) and the RXD pin on the HC-12 connects to pin 6 on the Arduino (Yellow).

Once again we connect the antenna the same way as before. I picked up a plastic Arduino Uno project box that just snaps around it, but I had to drill a hole in the plastic to be able to mount the antenna. All in all I was pretty satisfied with the outcome:

The receiver inside its project box

Programming the Receiver

The Arduino sketch for the receiver is pretty simple. The basic goal is to take the output from the HC-12 radio and reprint it to the USB Serial output so the computer can process it. The HC-12 is connected to ports 5 and 6 which will be assigned to SoftwareSerial with the statement SoftwareSerial HC12().

Since the receiver is connected to the computer, we are going to add some functionality that will allow us to configure it if we need to. There’s an amazing post by Mark Hughes that covers the HC-12 system in detail, including how to use the set pin. Practically all of the sketch for the receiver came from this post (I even left in the comments for you since he did such a great job), and I spent hours just messing around with the radios to understand them well enough to feel comfortable trusting them in my rocket.

#include <SoftwareSerial.h>

const byte HC12RxdPin = 6;                      // "RXD" Pin on HC12
const byte HC12TxdPin = 5;                      // "TXD" Pin on HC12
const byte HC12SetPin = 4;                      // "SET" Pin on HC12

unsigned long timer = millis();                 // Delay Timer

char SerialByteIn;                              // Temporary variable
char HC12ByteIn;                                // Temporary variable
String HC12ReadBuffer = "";                     // Read/Write Buffer 1 for HC12
String SerialReadBuffer = "";                   // Read/Write Buffer 2 for Serial
boolean SerialEnd = false;                      // Flag to indicate End of Serial String
boolean HC12End = false;                        // Flag to indiacte End of HC12 String
boolean commandMode = false;                    // Send AT commands

// Software Serial ports Rx and Tx are opposite the HC12 Rx and Tx
// Create Software Serial Port for HC12
SoftwareSerial HC12(HC12TxdPin, HC12RxdPin);

void setup() {

  HC12ReadBuffer.reserve(64);                   // Reserve 64 bytes for Serial message input
  SerialReadBuffer.reserve(64);                 // Reserve 64 bytes for HC12 message input

  pinMode(HC12SetPin, OUTPUT);                  // Output High for Transparent / Low for Command
  digitalWrite(HC12SetPin, HIGH);               // Enter Transparent mode
  delay(80);                                    // 80 ms delay before operation per datasheet
  Serial.begin(9600);                           // Open serial port to computer
  HC12.begin(9600);                             // Open software serial port to HC12
}

void loop() {

  while (HC12.available()) {                    // While Arduino's HC12 soft serial rx buffer has data
    HC12ByteIn = HC12.read();                   // Store each character from rx buffer in byteIn
    HC12ReadBuffer += char(HC12ByteIn);         // Write each character of byteIn to HC12ReadBuffer
    if (HC12ByteIn == '\n') {                   // At the end of the line
      HC12End = true;                           // Set HC12End flag to true
    }
  }

  while (Serial.available()) {                  // If Arduino's computer rx buffer has data
    SerialByteIn = Serial.read();               // Store each character in byteIn
    SerialReadBuffer += char(SerialByteIn);     // Write each character of byteIn to SerialReadBuffer
    if (SerialByteIn == '\n') {                 // Check to see if at the end of the line
      SerialEnd = true;                         // Set SerialEnd flag to indicate end of line
    }
  }

  if (SerialEnd) {                              // Check to see if SerialEnd flag is true

    if (SerialReadBuffer.startsWith("AT")) {    // Has a command been sent from local computer
      HC12.print(SerialReadBuffer);             // Send local command to remote HC12 before changing settings
      delay(100);                               //
      digitalWrite(HC12SetPin, LOW);            // Enter command mode
      delay(100);                               // Allow chip time to enter command mode
      Serial.print(SerialReadBuffer);           // Echo command to serial
      HC12.print(SerialReadBuffer);             // Send command to local HC12
      delay(500);                               // Wait 0.5s for a response
      digitalWrite(HC12SetPin, HIGH);           // Exit command / enter transparent mode
      delay(100);                               // Delay before proceeding
    } else {
      HC12.print(SerialReadBuffer);             // Transmit non-command message
    }
    SerialReadBuffer = "";                      // Clear SerialReadBuffer
    SerialEnd = false;                          // Reset serial end of line flag
  }

  if (HC12End) {                                // If HC12End flag is true
    if (HC12ReadBuffer.startsWith("AT")) {      // Check to see if a command is received from remote
      digitalWrite(HC12SetPin, LOW);            // Enter command mode
      delay(100);                               // Delay before sending command
      Serial.print(SerialReadBuffer);           // Echo command to serial.
      HC12.print(HC12ReadBuffer);               // Write command to local HC12
      delay(500);                               // Wait 0.5 s for reply
      digitalWrite(HC12SetPin, HIGH);           // Exit command / enter transparent mode
      delay(100);                               // Delay before proceeding
      HC12.println("Remote Command Executed");  // Acknowledge execution
    } else {
      Serial.print(HC12ReadBuffer);             // Send message to screen
    }
    HC12ReadBuffer = "";                        // Empty buffer
    HC12End = false;                            // Reset flag
  }
}

Note how the HC12SetPin is brought low in order to execute AT commands. This comes in really handy when troubleshooting, because you can then just send “AT” across the Serial Monitor, and it will tell you the status of the system. I’m not sure if all of the delays are really necessary, but I feel like he knows a lot more about this than I do, so I’ll just leave it as-is.

If you need to configure the radio on the onboard telemetry transmitter you can always repurposes the above sketch to work with the Nano. Just remember the pins are different, and there will be no SoftwareSerial.

If you want a reference guide for the HC-12 component you can check out the datasheet, which clearly describes all of the AT codes, functions and limitations of the system. As a general rule, you will be able to find datasheets for almost every component you will use in projects like this, including the individual components on the Arduino, such as the IMU. I’ll show you how to edit the drivers using information in the datasheet to get more functionality from your onboard SoC’s in a later post.

Radio Telemetry – Choosing the right wireless transceiver (4)

(I try to link to the products I used so you can find them more easily. If you purchase them from these links I may receive compensation from affiliate programs. I am not employed or influenced by the manufacturers or distributors.)

Enough about imaginary fuselages and prototyping boards with everything built in, LET’S BUILD SOMETHING!

In my last post we proved that the Arduino Nano 33 BLE Sense had all of the skills necessary to track attitude and some movement, but how do we get this information to the computer for analysis?

There’s a great video from BPS.space that includes a segment about all kinds of ways to transmit and save telemetry data, and I definitely should have watched this before my first flight. It boils down to two different options – onboard storage and wireless transmission.

With onboard storage you can use an SD card writer such as the ADA254 or a flash module such as this one from Amazon. The basic idea is that the builtin memory on the Arduino Nano erases itself every time the system is rebooted to make room for the new data the program will have to process. If we want to store the data more permanently then we would have to add that capability ourselves.

Both of these add-ons use the I2C communication built into the Arduino platform, so we can add as many as we would like, but there are benefits and drawbacks to each. An SD card writer would allow us to write the data to a removable card that we can plug into just about any smartphone or read with an SD card reader on almost any computer. The downside is this storage can sometimes be a little slower to write or read, and because the card is mechanically connected to the pins on the writer with pressure from the bracket, it might not withstand the stresses of rocket flight well, potentially resulting in data loss or a lost SD card.

The flash memory add-on is a great option because it can be faster to write to, and doesn’t have the mechanical weaknesses of the SD card, but the major drawback is that you can only retrieve the data by plugging the flight computer into the USB port on your main computer to download the data.

Both of these are inconvenient as primary data transmission methods because neither provide realtime data monitoring. Also, if the rocket flight ends in rapid unscheduled disassembly or a vertically premature arboreal landing, the data is gone for good. More on what I’ll call RUD and VPAL later.

All of these systems are small and lightweight, so we don’t have to worry about payload limits, but I’d really only be interested in onboard storage as a backup, in case wireless telemetry cut out mid-flight. My primary telemetry option needs to be wireless.

In order to achieve wireless communications you typically want to use Radio Frequency communications, which simply means that it’s sending the signal using Electromagnetic waves. One alternative would be light, like the infrared diode and sensor used for television remotes. This isn’t very effective on bright sunny days, though, and can be interrupted by visually opaque interference, such as rocket exhaust.

Another RF alternative would be sound, but the frequency is too low to be able to fit a reasonable amount of data in the signal, and it travels through the air too slowly (our rocket could reach 0.8x the speed of sound). Also rocket launches are audibly noisy, which makes for an all around poor environment for audio communications.

Luckily there are quite a few available RF options for the Arduino. The Arduino Nano 33 BLE Sense even has a built in Bluetooth LE SoC (System on a Chip). Bluetooth LE is a great technology for close-range wireless communications, and it would offer some cool smartphone integrations, but at 100m max range it’s just not strong enough for a rocket that’s built for 1km flight. This limitation also rules out any WiFi options.

The main reason why WiFi and BLE are poor options is because they use higher frequencies to transmit more data at a time. The tradeoff with higher frequencies is that it requires more input power to generate the same amplitude of electromagnetic wave. Since our telemetry system will just be powered by a typical 9v battery, we need a lower frequency radio to be able to transmit further without requiring more power than the battery can supply.

Another design consideration in the US is which frequencies we are legally able to transmit on. Luckily most of the devices that we can purchase online are pre-approved by the FCC and don’t require licenses to operate, but you always want to be careful and make sure you choose a frequency that won’t interfere with important uses such as emergency services, aircraft operation, or life-sustaining medical devices.

US Radio Spectrum Frequency Allocation Chart

The massive chart above shows all of the frequency allocations in the US. Needless to say it’s probably just easier to google search the frequencies you would like to use and read what people are posting about them than to try to figure out licensing from a chart like this.

Installing the HC-12 RF Transciever on the Arduino Nano

I started out building with the HC-12 RF chip, which uses the 433MHz band. These chips are great because they’re tiny, cheap, low power and the low frequency gives them a longer range for less power input. The (massive) downside is that transmitting legally on the 433MHz band requires a HAM (amateur radio operator) license. I don’t have one of these, I don’t want fines, and it will take me a few weeks at least to study and take the test.

I was able to find an alternative 2.4Ghz chip that is somewhat larger with a more limited range, but since my first flights will not be very high power, I probably don’t need the full 1Km range yet anyway.

When you’re prototyping electronics you’re going to need to plug and unplug wires quite frequently, so you don’t really want to be soldering every connection every time (although even when I solder I quite frequently have to undo and redo my wiring). There are a few things that you will need, like resistors, a solderless breadboard, and some connectors. To make this easier I just picked up an electronics project kit that included every component I could possibly ever need. I also have a separate connector kit that I can either use on solderless breadboards, or to make more precise runs on a soldered circuit board.

Prototyping the HC-12 using a solderless breadboard

Another great thing about the kit is that it comes with a knockoff Arduino Uno. I wired this up to another RF module with the same configuration as the onboard system, and it’s ready to rock!

In my next post I’ll discuss exactly how you wire the transceivers to the Arduinos, and the code that is required to run them.