Sensor Software Specs
Reading in data. Simple math
Intro
This document provides telemetry software specifications for the 2022 Swallow.
The Arduino will receive input from five (5) thermistors, five (5) voltage dividers, and two (2) ammeters. The input comes in as a raw voltage. This value needs to be interpreted and converted into the actual number measurement (degrees Celsius, Volts, Amps/Coulombs) before it can be sent along to the PixHawk via MavLINK.
Remember to comment your code well, and follow style conventions in indentation and bracket positioning.
Thermistors
Temperature readings come from the thermistors.
Logic
The thermistor has a varying resistance depending on the temperature. As we cannot measure resistance directly, we will instead be measuring the voltage drop across the thermistor.
Variables
The thermistor has intrinsic properties that are necessary to interpret its output. These can be acquired from its datasheet, or from its Amazon listing. These properties include:
- Nominal resistance
- Nominal temperature
- B-coefficient
In addition, we need to know the following constants about our circuit:
- Series resistor
- Sample group size
- Thermistor pin
These constants should be set at the top of the code for ease of access. I recommend using #define
.
Algorithm
First, read in the required number of samples in the group, all in quick succession. This is done using analogRead()
in a for-loop.
// take N samples in a row, with a slight delay
for (i=0; i< NUMSAMPLES; i++) {
samplesTotal += analogRead(THERMISTORPIN);
delay(10);
}
Next, we need to find the average of these several samples. Taking an average of a few samples each time helps reduce noise and improves reliability of our readings. This is simple math:
Now, we have a reliable value for the reading, we need to convert this reading into the actual resistance exhibited by the thermistor. We do this via an application of the following formulae:
// convert the value to resistance
average = 1023 / average - 1;
average = SERIESRESISTOR / average;
Now that we have the resistance values, we need to convert it to a temperature reading. The formula used for this is the Steinhart-Hart Equation for Thermistors. I did not read all the linear algebra in that Wikipedia article, and I don’t expect you to, either. Here is the code that performs the required calculations (I stole it from somewhere):
float steinhart;
steinhart = average / THERMISTORNOMINAL; // (R/Ro)
steinhart = log(steinhart); // ln(R/Ro)
steinhart /= BCOEFFICIENT; // 1/B * ln(R/Ro)
steinhart += 1.0 / (TEMPERATURENOMINAL + 273.15); // + (1/To)
steinhart = 1.0 / steinhart; // Invert
steinhart -= 273.15; // convert abs temp to C
We are now left with a floating-point number denoting the temperature reading, in Celsius. This entire program should be run repeatedly and frequently.
Voltage Dividers
The voltage readings we want are too large to be measured directly, so we are using a voltage divider.
Logic
The voltage divider circuit, wired into the Arduino’s inputs, is a way for us to indirectly measure large voltages. As the name suggests, a voltage divider gives us a fraction of the actual voltage. This fraction is constant, so the math to calculate the real value should not be too hard.
Variables
Recall that time you read all the way through the Requirements Sheet, and saw this diagram.
As you might guess, you will need to know the values of R1 and R2 to be able to figure out the voltage ratio. These values are probably going to be either 64 and 136 and 64 and 272, or 50 and 150 and 50 and 300.
Regardless, it is important to know accurately what these values are. Use a multimeter to measure the resistances, ask someone who knows, or use this handy website to interpret all the pretty colors.
Additionally, as in the Thermistor example, we may want to take several measurements as a group. We will therefore need a constant that sets this number of measurements.
Finally, we need the pin number where the voltage divider (Vout in the diagram) is connected.
To summarize, we need:
- R1 resistance (the one in series with R2)
- R2 resistance (the one we are measuring across)
- Number of samples
- Pin number
Algorithm
First, we take several measurements of the voltage. We again use a for-loop and analogRead().
// take N samples in a row, with a slight delay
for (i=0; i< NUMSAMPLES; i++) {
samplesTotal += analogRead(THERMISTORPIN);
delay(10);
}
Next, we again take the average of all the samples. This reduces noise and improves precision.
Now, we have a denoised input value. By the nature of the voltage divider, this input value is a set fraction of the actual voltage. Specifically, this fraction is the same as the proportion of the total resistance that is provided by R2. As such, we can divide by this ratio to get the original value:
Ammeters
The ammeters are more sophisticated sensors. They provide current readings over a data bus.
Logic
The ammeter measures the current running through it. It cannot withstand the full might of our motor battery current, so when we put the ammeters in series with the load, we split the current between the ammeter and a parallel shunt resistor.
Since the resistances of the shunt resistor and the ammeter are precisely known, we can calculate the actual current from the value of the fraction that we read in — like a current divider of sorts, reminiscent of what we just did in the previous section.
Variables
Since we are splitting the current between two branches and only measuring one of them, we need to know their resistance values to know precisely what fraction of the total current passes through the ammeter.
We need to know the values of R1 and R2. We will also be taking several measurements, as in the other examples, so once again we need the number of samples taken as a variable.
We will also be using the ammeter’s library, so make sure to import it and include it at the top:
To use this library, each device needs an “address” on the bus. This is equivalent to the pin number. This is set by the sensor’s hardware. Consult the documentation for more information about ammeter addresses.
In summary:
- R1 (shunt resistor)
- R2 (ammeter resistor)
- Number of samples
- Device address (one of: 40, 41, 44, 45)
Algorithm
The most important distinction between the code used for the ammeter and the code for the other sensors is that here, we will be reading inputs using the sensor’s own library methods.
First, check that you remembered to install and #include the Wire library, like I told you to.
Now, instantiate an instance of the sensor (outside of the main loop):
In setup(), after initializing Serial, make sure that the sensor is connected and found:
Serial.begin(115200);
// Wait until serial port is opened
while (!Serial) { delay(10); }
Serial.println("Adafruit INA260 Test");
if (!ina260.begin((uint8_t)ADDRESS))) {
Serial.println("Couldn't find INA260 chip");
while (1);
}
Serial.println("Found INA260 chip");
prevTime = millis();
totalCharge = 0;
Now, we can move on to the main body of our code.
As always, we begin by reading in several values, only this time we use the readCurrent method instead of analogRead.
// take N samples in a row, with a slight delay
for (i=0; i< NUMSAMPLES; i++) {
samplesTotal += ina260.readCurrent();
delay(10);
}
Of course we then have to average out the samples:
We now have to convert the reading into the actual value using the known ratio. We have the equation R1i1 = R2i2, and we have the values of R1, R2, and i2 As such, we can get iTOTAL = i1 + i2 = R2i2/R1 + i2
We can now do some precalculus to find the amount of charge that has been used since the last charge. We can add this amount to the variable keeping track of the total charge used.
currTime = millis();
timeDelta = currTime - prevTime;
chargeUsed = timeDelta/1000 * current;
totalCharge += chargeUsed;
prevTime = currTime();
Some of this code is from the official resource.
Putting it All Together
Here’s the current version of the code:
#include <Adafruit_INA260.h>
//numbers of samples for temperature, voltage, and current
#define T_NUM_SAMPLES 10
#define V_NUM_SAMPLES 10
#define C_NUM_SAMPLES 10
int max_samples = max(T_NUM_SAMPLES, max(V_NUM_SAMPLES, C_NUM_SAMPLES));
//<------ temperature variables ------>
int t_nominal_res = 100000; //100k
int t_nominal_temp = 20;
int t_B = 3950; //this is a wild guess
int t_R_series = 10000; //10k
// sum of samples taken
float t_sample_sum_air = 0;
float t_sample_sum_esc = 0;
float t_sample_sum_jetson = 0;
float t_sample_sum_motor_batt = 0;
float t_sample_sum_pdb_batt = 0;
//pins
int t_pin_air = A1;
int t_pin_esc = A2;
int t_pin_jetson = A3;
int t_pin_motor_batt = A4;
int t_pin_pdb_batt = A5;
//temperatures
float t_air;
float t_esc;
float t_jetson;
float t_motor_batt;
float t_pdb_batt;
//<------ voltage variables ------>
// sum of samples taken
float v_sample_sum_pack = 0;
float v_sample_sum_1 = 0;
float v_sample_sum_2 = 0;
float v_sample_sum_3 = 0;
float v_sample_sum_4 = 0;
// calculated voltage
float v_value_pack = 0;
float v_value_1 = 0;
float v_value_2 = 0;
float v_value_3 = 0;
float v_value_4 = 0;
//resistors
int v_R1_pack = 300000; //300k
int v_R2_pack = 50000; //50k
int v_R1_batt = 150000; //150k
int v_R2_batt = 50000; //50k
//pin numbers
int v_pin_pack = A10;
int v_pin_1 = A11;
int v_pin_2 = A12;
int v_pin_3 = A13;
int v_pin_4 = A14;
//<------ current variables ------>
// sum of samples taken
float c_sample_sum_motor = 0;
float c_sample_sum_pdb = 0;
//resistors
int c_R1_motor;
int c_R2_motor;
int c_R1_pdb;
int c_R2_pdb;
//device addresses
int c_address_motor = 40;
int c_address_pdb = 41;
//timekeeping values
long prevTime;
long currTime;
//initialize sensors
Adafruit_INA260 c_sensor_motor = Adafruit_INA260();
Adafruit_INA260 c_sensor_pdb = Adafruit_INA260();
//total charge
long double totalCharge = 0;
//<----- end variables ----->
void setup() {
// put your setup code here, to run once:
prevTime = millis();
// Wait until serial port is opened
while (!Serial) { delay(10); }
Serial.println("Adafruit INA260 motor Test");
if (!c_sensor_motor.begin((uint8_t)c_address_motor)) {
Serial.println("Couldn't find INA260 motor chip");
while (1);
}
Serial.println("Found INA260 motor chip");
Serial.println("Adafruit INA260 pdb Test");
if (!c_sensor_pdb.begin((uint8_t)c_address_pdb)) {
Serial.println("Couldn't find INA260 pdb chip");
while (1);
}
Serial.println("Found INA260 pdb chip");
}
void loop() {
// put your main code here, to run repeatedly:
// collect samples
for (int i=0; i< max_samples; i++) {
if(i<T_NUM_SAMPLES){
t_sample_sum_air += analogRead(t_pin_air);
t_sample_sum_esc += analogRead(t_pin_esc);
t_sample_sum_jetson += analogRead(t_pin_jetson);
t_sample_sum_motor_batt += analogRead(t_pin_motor_batt);
t_sample_sum_pdb_batt += analogRead(t_pin_pdb_batt);
}
if(i<V_NUM_SAMPLES){
v_sample_sum_pack += analogRead(v_pin_pack);
v_sample_sum_1 += analogRead(v_pin_1);
v_sample_sum_2 += analogRead(v_pin_2);
v_sample_sum_3 += analogRead(v_pin_3);
v_sample_sum_4 += analogRead(v_pin_4);
}
if(i<C_NUM_SAMPLES){
c_sample_sum_motor += c_sensor_motor.readCurrent();
c_sample_sum_pdb += c_sensor_pdb.readCurrent();
}
delay(10);
}
////<----- average samples ----->
float t_avg_air = t_sample_sum_air/T_NUM_SAMPLES;
float t_avg_esc = t_sample_sum_esc/T_NUM_SAMPLES;
float t_avg_jetson = t_sample_sum_jetson/T_NUM_SAMPLES;
float t_avg_motor_batt = t_sample_sum_motor_batt/T_NUM_SAMPLES;
float t_avg_pdb_batt = t_sample_sum_pdb_batt/T_NUM_SAMPLES;
float v_avg_pack = v_sample_sum_pack/V_NUM_SAMPLES;
float v_avg_1 = v_sample_sum_1/V_NUM_SAMPLES;
float v_avg_2 = v_sample_sum_2/V_NUM_SAMPLES;
float v_avg_3 = v_sample_sum_3/V_NUM_SAMPLES;
float v_avg_4 = v_sample_sum_4/V_NUM_SAMPLES;
float c_avg_motor = c_sample_sum_motor/C_NUM_SAMPLES;
float c_avg_pdb = c_sample_sum_pdb/C_NUM_SAMPLES;
float steinhart;
//<----- calculate temperatures ----->
//air
t_avg_air = 1023.0 / t_avg_air - 1.0;
t_avg_air = t_R_series / t_avg_air;
steinhart = t_avg_air / t_nominal_res; // (R/Ro)
steinhart = log(steinhart); // ln(R/Ro)
steinhart /= t_B; // 1/B * ln(R/Ro)
steinhart += 1.0 / (t_nominal_temp + 273.15); // + (1/To)
steinhart = 1.0 / steinhart; // Invert
steinhart -= 273.15; // convert abs temp to C
t_air = steinhart;
//esc
t_avg_esc = 1023.0 / t_avg_esc - 1.0;
t_avg_esc = t_R_series / t_avg_esc;
steinhart = t_avg_esc / t_nominal_res; // (R/Ro)
steinhart = log(steinhart); // ln(R/Ro)
steinhart /= t_B; // 1/B * ln(R/Ro)
steinhart += 1.0 / (t_nominal_temp + 273.15); // + (1/To)
steinhart = 1.0 / steinhart; // Invert
steinhart -= 273.15; // convert abs temp to C
t_esc = steinhart;
//jetson
t_avg_jetson = 1023.0 / t_avg_jetson - 1.0;
t_avg_jetson = t_R_series / t_avg_jetson;
steinhart = t_avg_jetson / t_nominal_res; // (R/Ro)
steinhart = log(steinhart); // ln(R/Ro)
steinhart /= t_B; // 1/B * ln(R/Ro)
steinhart += 1.0 / (t_nominal_temp + 273.15); // + (1/To)
steinhart = 1.0 / steinhart; // Invert
steinhart -= 273.15; // convert abs temp to C
t_jetson = steinhart;
//motor battery
t_avg_motor_batt = 1023.0 / t_avg_motor_batt - 1.0;
t_avg_motor_batt = t_R_series / t_avg_motor_batt;
steinhart = t_avg_motor_batt / t_nominal_res; // (R/Ro)
steinhart = log(steinhart); // ln(R/Ro)
steinhart /= t_B; // 1/B * ln(R/Ro)
steinhart += 1.0 / (t_nominal_temp + 273.15); // + (1/To)
steinhart = 1.0 / steinhart; // Invert
steinhart -= 273.15; // convert abs temp to C
t_motor_batt = steinhart;
//pdb battery
t_avg_pdb_batt = 1023.0 / t_avg_pdb_batt - 1.0;
t_avg_pdb_batt = t_R_series / t_avg_pdb_batt;
steinhart = t_avg_pdb_batt / t_nominal_res; // (R/Ro)
steinhart = log(steinhart); // ln(R/Ro)
steinhart /= t_B; // 1/B * ln(R/Ro)
steinhart += 1.0 / (t_nominal_temp + 273.15); // + (1/To)
steinhart = 1.0 / steinhart; // Invert
steinhart -= 273.15; // convert abs temp to C
t_pdb_batt = steinhart;
//<----- calculate voltages ----->
float v_ratio_pack = (1.0 * v_R2_pack) / (v_R1_pack + v_R2_pack);
float v_ratio_batt = (1.0 * v_R2_batt) / (v_R1_batt + v_R2_batt);
float v_pack = v_avg_pack / v_ratio_pack;
float v_1 = v_avg_1 / v_ratio_batt;
float v_2 = v_avg_2 / v_ratio_batt;
float v_3 = v_avg_3 / v_ratio_batt;
float v_4 = v_avg_4 / v_ratio_batt;
//<----- calculate currents ----->
double c_motor = c_avg_motor + (c_R2_motor * c_avg_motor)/c_R1_motor;
double c_pdb = c_avg_pdb + (c_R2_pdb * c_avg_pdb)/c_R1_pdb;
//<----- calculate charge used ----->
currTime = millis();
double timeDelta = currTime - prevTime;
double chargeUsed = timeDelta/1000 * c_motor;
totalCharge += chargeUsed;
prevTime = currTime;
}