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.

Basic Telemetry – What is the rocket doing? (3)

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

One important goal for this project is to be able to make automated in-flight decisions and monitor flight progress. To accomplish this we need some kind of flight computer.

Something you may not know about me – I love Arduinos. With just a tiny amount of programming and wiring knowledge you can build almost anything. The Italian rapid prototyping board has dramatically changed the world of inventing and product design. The Arduino system has been used not only by hobbyists, but educators, NASA and even the Department of Defense have been known to employ the tiny prototyping boards in research and development projects.

If we’re planning to keep the body as small as possible, an obvious choice would be the Arduino Nano because of its size. You can even try the Seeeduino, an offbrand controller that claims to be the smallest arduino-based controller on the market.

I wanted more than just to launch a controller board into the air, though. I wanted to know what it was doing. Where is it going? How fast? How high?

To be able to answer these questions you need a few components. The first is called an IMU or Inertial Measurement Unit. IMU capabilities are measured in axes, and axes are grouped into 3’s based on what they measure. The three axes of any 3-dimensional system are X, Y and Z. Think of a 3-dimensional graph:

The three axes, wikipedia.com

In order to understand how something moves in space you need to understand translation and rotation, which make up the first two types of sensor in an IMU. Translation is measured by an accelerometer, linear acceleration and deceleration (which is just negative acceleration) is quantified in g’s of force, which can be best explained as how strong the acceleration force is in comparison to gravity. One g is the same amount of force as you feel pulling you down at sea level, and 5g is five times that amount of force.

Rotation is measured by a gyrometer, which uses degrees per second (dps). 360dps on the z axis means you turn a full circle around that axis in one second. We sometimes refer to these motions by the Euler Angles – “pitch”, “roll” and “yaw” – when talking about flight. This is more difficult with rockets, since rocket bodies are typically symmetrical in two axes, so in rocketry we may describe the rocket as “rolling” if it is turning around the fuselage, but I am less likely to discuss “pitch” and will probably not use the term “yaw”.

Either way, here is a diagram from Wikipedia that more clearly explains how those terms are used in aircraft:

The Euler Angles as they apply to an aircraft

I mentioned that an IMU generally has three types of sensors, but so far I have only described two of them. The third one is called a magnetometer, and it is essentially a three dimensional compass. Magnetometers are essential because they can help tell us the absolute orientation, or attitude, of the rocket.

Now that we know how the IMU works, we need to figure out how to add one to the telemetry system. We could pick up one of the Arduino Nanos I mentioned earlier and connect it with an external IMU board, and there’s nothing wrong with this EXCEPT I want something smaller, and I love integrated components!

After some digging I found that I had started this project right on time, since Arduino had recently released the Arduino Nano 33 BLE Sense with integrated 9-axis IMU (and a lot of other goodies). This meant a simple plug-and-play solution that I could use to measure attitude and motion in flight, and it also meant access to an onboard thermometer, hygrometer and even a barometer, which I could use for measuring altitude.

I won’t go into the details of how to calculate altitude by using barometric pressure, but it is based on the principle that air pressure is generally higher the lower you are in altitude. As the rocket climbs, then, the air pressure should decrease, and some mathematical formulas allow us to use the change in pressure over time to estimate how what altitude we have reached, and we can use the rate of change to determine what stage of flight we are currently in.

Arduino Nano 33 Ble Sense – Amazon.com

Once the telemetry board (new designation) arrived I hooked it up to my computer and wrote a quick program that fed the signals back through the built in serial communications. I noticed that the data was a little noisy (the image below is of me shaking it around – not what I’m talking about) so I added some moving average logic to smooth out the values. Ultimately I took this logic out because I determined logging the raw values would result in the best data, and I could always do smoothing with the raw data at a later time.

In my next post I will discuss wireless telemetry, and the wonderful world of RF comms (Radio Frequency communications).

Arduino IMU data plotted in “Data Monitor”, sketch showing data smoothing