/*
*-----------------------------------------------------------------------------
*
* IV_Swinger2.ino: IV Swinger 2 Arduino sketch
*
* Copyright (C) 2017-2022 Chris Satterlee
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
*-----------------------------------------------------------------------------
*
* IV Swinger and IV Swinger 2 are open source hardware and software
* projects
*
* Permission to use the hardware designs is granted under the terms of
* the TAPR Open Hardware License Version 1.0 (May 25, 2007) -
* http://www.tapr.org/OHL
*
* Permission to use the software is granted under the terms of the GNU
* GPL v3 as noted above.
*
* Current versions of the licensing files, documentation, Fritzing file
* (hardware description), and software can be found at:
*
* https://github.com/csatt/IV_Swinger
*
*-----------------------------------------------------------------------------
*
* This file contains the Arduino sketch for the IV Swinger 2. It
* performs the following functions:
*
* - Participates in handshakes with the host computer (via USB)
* - Receives configuration options from the host
* - Communicates debug messages to the host
* - Controls the relay that switches the capacitor between the
* bleed circuit and the PV circuit
* - Reads and records values from the two ADC channels
* - Waits for current to stabilize at the beginning
* - Compensates for the fact that time passes between the current
* and voltage measurements
* - Selectively discards values so that the Arduino memory isn't
* exhausted before the IV curve is complete
* - Determines when the IV curve is complete
* - Sends results to the host
*
* Performance is important. The rate that the curve is "swung" is a
* function of the capacitor value and the PV module; there is no way to
* slow it down (other than using a larger capacitance). The faster the
* software can take measurements, the closer together the points will
* be, which improves the "resolution" of the IV curve. Because i = C *
* dv/dt, the speed of the sweep is not constant from the Isc end of the
* curve to the Voc end. It is faster (i.e. dt is smaller) when current
* (i) is higher and when the voltage change (dv) between points is
* lower. At the beginning of the curve, i is high, but dv is also high,
* so the sweep speed is moderate. And at the end of the curve, both i
* and dv are low, so the sweep speed is also moderate. But just past
* the knee, i is still high but dv is low, so the sweep rate is the
* highest. If the software performance is poor, this part of the curve
* will have poor resolution.
*
* The downside of taking measurements quickly is that too many
* measurements are taken during the parts of the curve where the sweep
* rate is low. The Arduino has very limited memory, so if all these
* points are recorded, memory will be exhausted before the curve is
* complete. The software must selectively discard points to prevent
* this from happening. The trick is to determine which points to
* discard. It is not useful to have points that are very close to each
* other, so the discard criterion is based on the distance between
* points. This calculation has to be very fast because it is performed
* after every measurement, and that reduces the rate that measurements
* can be taken. Any use of floating point math, or even 32-bit (long)
* math slows things down drastically, so only 16-bit integer math is
* used. Instead of Pythagorean distance, so-called Manhattan distance
* is used, which only requires subtraction and addition. The criterion
* distance could be a constant, but that would not produce good results
* for all IV curves. Instead, it is scaled based on the measured values
* for Voc and Isc. The Voc values are read before the relay is
* activated and the Isc values are determined just after the relay is
* activated. The minimum distance criterion calculation requires some
* computation time between the first two measured points, but that is
* not normally a resolution-sensitive part of the curve. Nevertheless,
* this code is also restricted to simple 16-bit integer math in order
* to make it as quick as possible.
*
* A single point on the curve requires reading both channels of the
* ADC. There is no way to read both values at the same time; each read
* requires a separate SPI transaction, so some time passes between the
* two reads, and the values do not represent the exact same point in
* time. The simplest way to deal with this would be to ignore it; if
* the points are close enough together, the effect is relatively
* minor. But it isn't difficult to compensate for, so we do. One way
* to compensate would be to do three reads for each pair
* (i.e. CH0/CH1/CH0 or CH1/CH0/CH1) and average the first and third.
* But that would slow things down by 50%. Instead, we just do one read
* of each channel on each iteration, but interpolate between the CH1
* values of each iteration. The catch is that there is computation
* between iterations (which takes time), so it's not a simple average;
* it's a weighted average based on measured times.
*
* Cell version support:
*
* The Arduino code does exactly the same thing for a cell version IVS2
* as it does for the module version. There is, however, a configuration
* command that the host software uses to turn on or turn off the second
* relay.
*
* SSR support:
*
* This code supports both the original electromechanical relay (EMR)
* designs and the more recent solid-state relay (SSR) designs. The
* code does not "know" which type of relay is in use. The EMR is an
* SPDT switch, so only one is needed to switch between the bleed
* circuit and the PV circuit. SSRs are SPST, so two are required for
* the same functionality (SSR1 and SSR2). A third SSR is needed because
* of the slow turn-on time of SSR1 (7.5ms typical). SSR3, when active,
* bypasses the load capacitors. SSR1 is controlled by the same Arduino
* pin as the EMR (pin D2). SSR2 is controlled by a different pin
* (D6). SSR1 is turned on ("closed") and SSR2 is turned off ("opened")
* after the Voc measurement has been taken - as usual. SSR3,
* controlled by Arduino pin D7, is turned on at this point as well
* (actually just before). In this state, the PV current flows through
* SSR1 and SSR3 and through the shunt. The load capacitors do not
* start charging yet, and the current has a very near short-circuit
* path. The Isc polling is started at this point. When the voltage and
* current stop changing, SSR3 is turned off ("opened"), the load
* capacitors start to charge, and the curve is traced. When the curve
* is complete, SSR1 is turned off, and SSR2 is turned on. In this
* state, the load capacitors drain through SSR2 and the bleed resistor.
* The relays stay in this state until the next curve is swung. Since
* nothing is connected to Arduino pins D6 and D7 in the EMR design,
* there is no effect of the code that is controlling SSR2 and SSR3. And
* since SSR1 activated (and SSR2 deactivated) at exactly the same times
* as the EMR (using the same Arduino pin for SSR1), the code does the
* right thing. [In a design without SSR3, the load capacitors would
* start charging up while SSR1 is still turning on. During the turn-on
* period, SSR1 has a significant resistance. By the time it is fully
* turned on, the load capacitors have a significant resistance. There
* is never a time when the PV "sees" anything close to a short circuit,
* and the curve is truncated on the Isc end. SSR3 provides a
* short-circuit path around the load capacitors, keeping them from
* charging until SSR1 is fully on.]
*
* The SSR cell version has four SSRs. SSR1 is the same as SSR1 in the
* module version, connected to Arduino pin D2. There is no SSR2 or SSR3
* in the cell version. SSR4 takes the place of SSR2 and SSR3 and is
* used both to bleed and to bypass the load capacitors since there is
* no bleed resistor. It is controlled by Arduino pin D8. SSR5 and SSR6
* together provide the function of the second relay in the non-SSR cell
* version. SSR5 is controlled by Arduino pin D4, just like the second
* relay. SSR6 is controlled by Arduino pin D5. As with the other SSRs,
* the versions that do not have SSR4, SSR5, and SSR6 are not affected
* when the pins controlling them are activated or deactivated since
* nothing is connected to those pins in the other versions.
*
* FET support:
*
* This code also supports the even newer FET-based design (module
* only). In place of SSR1, SSR2, and SSR3 are FET1, FET2, and FET3. The
* FETs are activated and deactivated at the same times as their SSR
* counterparts. The only difference is that FET3 is active-high,
* whereas SSR3 is active-low. To make this work without the code having
* to "know" which type the hardware is, FET3 is controlled by a
* different Arduino digital output pin from SSR3 (D9 instead of
* D7). Note that the "SSR" constant and variable names have not been
* changed to reflect that they now actually mean "SSR or FET".
*
*/
#define VERSION "1.4.6" // Version of this Arduino sketch
// Uncomment one or more of the following to enable the associated
// feature. Note, however, that enabling these features uses more of the
// Arduino's SRAM, so we have to reduce the maximum number of IV points
// accordingly to prevent running out of memory.
//#define DS18B20_SUPPORTED
//#define ADS1115_PYRANOMETER_SUPPORTED
//#define CAPTURE_UNFILTERED_ISC_POLL // Debug only
//#define CAPTURE_UNFILTERED_POST_ISC // Debug only
#if defined(CAPTURE_UNFILTERED_ISC_POLL) || \
defined(CAPTURE_UNFILTERED_POST_ISC)
#define CAPTURE_UNFILTERED
#endif
#include
#include
#ifdef DS18B20_SUPPORTED
#include
#include
#define DS18B20_SRAM 44
#else
#define DS18B20_SRAM 0
#endif
#ifdef ADS1115_PYRANOMETER_SUPPORTED
#include
#include
#define ADS1115_SRAM 224
#define ADS1115_IRRADIANCE_POLLING_LOOPS 10
#define ADS1115_TEMP_POLLING_LOOPS 5
#define MAX_STABLE_TEMP_ERR_PPM 5000 // 5000 = 0.5%
#define MAX_STABLE_IRRAD_ERR_PPM 10000 // 10000 = 1%
#else
#define ADS1115_SRAM 0
#endif
#ifdef CAPTURE_UNFILTERED
#define MAX_UNFILTERED_POINTS 125
#define UNFILTERED_SRAM ((MAX_UNFILTERED_POINTS*4)+12)
#else
#define UNFILTERED_SRAM 0
#endif
#define MAX_UINT (1<<16)-1 // Max unsigned integer
#define MAX_INT (1<<15)-1 // Max integer
#define MAX_ULONG (1LL<<32)-1 // Max unsigned long integer
#define MAX_LONG (1<<31)-1 // Max long integer
#define MAX_MSG_LEN 40 // Maximum length of a host message
#define MSG_TIMER_TIMEOUT 1000 // Number of times to poll for host message
#define CLK_DIV SPI_CLOCK_DIV8 // SPI clock divider ratio
#define SERIAL_BAUD 57600 // Serial port baud rate
#define ADC_MAX 4096.0 // Max count of ADC (2^^num_bits)
#define ADC_SAT (ADC_MAX-1) // ADC saturation count
#define ADC_CS_PIN 10 // Arduino pin used for ADC chip select
#define RELAY_PIN 2 // Arduino pin used to activate relay (or SSR1)
#define ONE_WIRE_BUS 3 // Arduino pin used for one-wire bus (DS18B20)
#define SECOND_RELAY_PIN 4 // Arduino pin used to activate 2nd relay/SSR5
#define SSR2_PIN 6 // Arduino pin used to activate SSR2 (if exists)
#define SSR2_ACTIVE HIGH // SSR2 is active high
#define SSR2_INACTIVE LOW // SSR2 is active high
#define SSR3_PIN 7 // Arduino pin used to activate SSR3 (if exists)
#define SSR3_ACTIVE LOW // SSR3 is active low
#define SSR3_INACTIVE HIGH // SSR3 is active low
#define FET3_PIN 9 // Arduino pin used to activate FET3 (if exists)
#define FET3_ACTIVE HIGH // FET3 is active high
#define FET3_INACTIVE LOW // FET3 is active high
#define SSR4_PIN 8 // Arduino pin used to activate SSR4 (if exists)
#define SSR4_ACTIVE LOW // SSR4 is active low
#define SSR4_INACTIVE HIGH // SSR4 is active low
#define SSR6_PIN 5 // Arduino pin used to activate SSR6 (if exists)
#define SSR6_ACTIVE LOW // SSR6 is active low
#define SSR6_INACTIVE HIGH // SSR6 is active low
#define CS_INACTIVE HIGH // Chip select is active low
#define CS_ACTIVE LOW // Chip select is active low
#define VOLTAGE_CH 0 // ADC channel used for voltage measurement
#define CURRENT_CH 1 // ADC channel used for current measurement
#define VOC_POLLING_LOOPS 400 // Number of loops measuring Voc
#define FULL_MAX_IV_POINTS 275 // Max number of I/V pairs to capture
#define IV_POINT_REDUCTION ((DS18B20_SRAM+ADS1115_SRAM+UNFILTERED_SRAM)/4)
#define MAX_IV_POINTS (FULL_MAX_IV_POINTS - IV_POINT_REDUCTION)
#define MAX_IV_MEAS 1000000 // Max number of I/V measurements (inc discards)
#define I_CH_1ST_WEIGHT 5 // Amount to weigh 1st I ADC value in avg calc
#define I_CH_2ND_WEIGHT 3 // Amount to weigh 2nd I ADC value in avg calc
#define MIN_ISC_ADC 100 // Minimum ADC count for Isc
#define MAX_ISC_POLL 5000 // Max loops to wait for Isc to stabilize
#define ISC_STABLE_ADC 5 // Stable Isc changes less than this
#define MAX_DISCARDS 300 // Maximum consecutive discarded points
#define MIN_VOC_ADC 10 // Minimum value for Voc ADC value
#define ASPECT_HEIGHT 2 // Height of graph's aspect ratio (max 8)
#define ASPECT_WIDTH 3 // Width of graph's aspect ratio (max 8)
#define TOTAL_WEIGHT (I_CH_1ST_WEIGHT + I_CH_2ND_WEIGHT)
#define AVG_WEIGHT (int) ((TOTAL_WEIGHT + 1) / 2)
#define EEPROM_VALID_VALUE 123456.7890 // Must match IV_Swinger2.py
#define EEPROM_RELAY_ACTIVE_HIGH_ADDR 44 // Must match IV_Swinger2.py
#define SSR_CAL_USECS 3000000 // Microseconds to perform SSR current cal
#define SSR_CAL_RD_USECS 100000 // Microseconds to read/average current
#define CMD_BDGP_READ_ITER 1000 // Bandgap iterations (on READ_BANDGAP command)
#define GO_BDGP_READ_ITER 1000 // Bandgap iterations (on every Go command)
// Compile-time assertion macros (from Stack Overflow)
#define COMPILER_ASSERT(predicate) _impl_CASSERT_LINE(predicate,__LINE__)
#define _impl_PASTE(a,b) a##b
#define _impl_CASSERT_LINE(predicate, line) \
typedef char _impl_PASTE(assertion_failed_on_line_,line)[2*!!(predicate)-1];
// Compile-time assertions
COMPILER_ASSERT(MAX_IV_POINTS >= 10);
COMPILER_ASSERT(MAX_IV_MEAS <= (unsigned long) MAX_ULONG);
COMPILER_ASSERT(TOTAL_WEIGHT <= 16);
COMPILER_ASSERT(ASPECT_HEIGHT <= 8);
COMPILER_ASSERT(ASPECT_WIDTH <= 8);
// Global variables
char relay_active;
char relay_inactive;
int clk_div = CLK_DIV;
int max_iv_points = MAX_IV_POINTS;
int min_isc_adc = MIN_ISC_ADC;
int max_isc_poll = MAX_ISC_POLL;
int isc_stable_adc = ISC_STABLE_ADC;
int max_discards = MAX_DISCARDS;
int aspect_height = ASPECT_HEIGHT;
int aspect_width = ASPECT_WIDTH;
const static char ready_str[] PROGMEM = "Ready";
const static char config_str[] PROGMEM = "Config";
const static char go_str[] PROGMEM = "Go";
const static char clk_div_str[] PROGMEM = "CLK_DIV";
const static char max_iv_points_str[] PROGMEM = "MAX_IV_POINTS";
const static char min_isc_adc_str[] PROGMEM = "MIN_ISC_ADC";
const static char max_isc_poll_str[] PROGMEM = "MAX_ISC_POLL";
const static char isc_stable_adc_str[] PROGMEM = "ISC_STABLE_ADC";
const static char max_discards_str[] PROGMEM = "MAX_DISCARDS";
const static char aspect_height_str[] PROGMEM = "ASPECT_HEIGHT";
const static char aspect_width_str[] PROGMEM = "ASPECT_WIDTH";
const static char write_eeprom_str[] PROGMEM = "WRITE_EEPROM";
const static char dump_eeprom_str[] PROGMEM = "DUMP_EEPROM";
const static char relay_state_str[] PROGMEM = "RELAY_STATE";
const static char second_relay_state_str[] PROGMEM = "SECOND_RELAY_STATE";
const static char do_ssr_curr_cal_str[] PROGMEM = "DO_SSR_CURR_CAL";
const static char read_bandgap_str[] PROGMEM = "READ_BANDGAP";
const static char read_adc_str[] PROGMEM = "READ_ADC";
#ifdef DS18B20_SUPPORTED
// Global setup for DS18B20 temperature sensor
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
int num_ds18b20s;
#endif
#ifdef ADS1115_PYRANOMETER_SUPPORTED
// Global setup for ADS1115-based pyranometer
Adafruit_ADS1115 ads1115;
#endif
void setup()
{
bool host_ready = false;
char incoming_msg[MAX_MSG_LEN];
// Get relay type from EEPROM (active-low or active-high)
relay_active = get_relay_active_val();
relay_inactive = (relay_active == LOW) ? HIGH : LOW;
// Initialization
pinMode(ADC_CS_PIN, OUTPUT);
digitalWrite(ADC_CS_PIN, CS_INACTIVE);
pinMode(RELAY_PIN, OUTPUT); // Also SSR1
digitalWrite(RELAY_PIN, relay_inactive);
pinMode(SECOND_RELAY_PIN, OUTPUT); // Also SSR5
digitalWrite(SECOND_RELAY_PIN, relay_inactive);
pinMode(SSR2_PIN, OUTPUT);
digitalWrite(SSR2_PIN, SSR2_ACTIVE);
pinMode(SSR3_PIN, OUTPUT);
digitalWrite(SSR3_PIN, SSR3_INACTIVE);
pinMode(FET3_PIN, OUTPUT);
digitalWrite(FET3_PIN, FET3_INACTIVE);
pinMode(SSR4_PIN, OUTPUT);
digitalWrite(SSR4_PIN, SSR4_ACTIVE);
pinMode(SSR6_PIN, OUTPUT);
digitalWrite(SSR6_PIN, SSR6_ACTIVE);
Serial.begin(SERIAL_BAUD);
SPI.begin();
SPI.setClockDivider(clk_div);
set_up_bandgap();
#ifdef DS18B20_SUPPORTED
// DS18B20 temperature sensor init
sensors.begin();
num_ds18b20s = sensors.getDS18Count();
if (num_ds18b20s) {
sensors.setResolution(10);
}
#endif
#ifdef ADS1115_PYRANOMETER_SUPPORTED
ads1115.begin();
#endif
// Print version number
Serial.print(F("IV Swinger2 sketch version "));
Serial.println(F(VERSION));
// Tell host that we're ready, and wait for config messages and
// acknowledgement
host_ready = false;
while (!host_ready) {
Serial.println(F("Ready"));
if (get_host_msg(incoming_msg)) {
if (strstr_P(incoming_msg, ready_str)) {
host_ready = true;
}
else if (strstr_P(incoming_msg, config_str)) {
process_config_msg(incoming_msg);
}
}
}
#ifdef DS18B20_SUPPORTED
Serial.println(F("DS18B20 temperature sensor is SUPPORTED"));
#else
Serial.println(F("DS18B20 temperature sensor is NOT supported"));
#endif
#ifdef ADS1115_PYRANOMETER_SUPPORTED
Serial.println(F("ADS1115-based pyranometer is SUPPORTED"));
#else
Serial.println(F("ADS1115-based pyranometer is NOT supported"));
#endif
#ifdef CAPTURE_UNFILTERED
Serial.println(F("Debug capture of unfiltered IV points is SUPPORTED"));
#else
Serial.println(F("Debug capture of unfiltered IV points is NOT supported"));
#endif
// Print value of MAX_IV_POINTS / max_iv_points
Serial.print(F("MAX_IV_POINTS: "));
Serial.print(MAX_IV_POINTS);
Serial.print(F(" max_iv_points: "));
Serial.println(max_iv_points);
#ifdef DS18B20_SUPPORTED
// Print temp sensor info
for (int ii = 0; ii < num_ds18b20s; ii++) {
DeviceAddress rom_code;
sensors.getAddress(rom_code, ii);
Serial.print(F("ROM code of DS18B20 temp sensor #"));
Serial.print(ii+1);
Serial.print(F(" is 0x"));
for (int jj = 7; jj >= 0; jj--) {
if (rom_code[jj] < 16) Serial.print(F("0"));
Serial.print(rom_code[jj], HEX);
}
Serial.println(F(""));
}
#endif
}
void loop()
{
// Arduino: ints are 16 bits
bool go_msg_received;
bool update_prev_i = false;
bool poll_timeout = false;
bool skip_isc_poll = false;
bool count_updated = false;
bool voc_adc_found = false;
bool emr_isc_stable = false;
bool ssr_isc_stable = false;
char incoming_msg[MAX_MSG_LEN];
int ii;
int index = 0;
int max_count = 0;
int adc_v_delta, adc_i_delta, adc_i_prev_delta;
int manhattan_distance, min_manhattan_distance;
int pt_num = 1; // counts points actually recorded
int isc_poll_loops = 0;
int num_discarded_pts = 0;
int i_scale, v_scale;
int adc_v_vals[MAX_IV_POINTS], adc_i_vals[MAX_IV_POINTS];
int isc_adc, voc_adc;
int adc_noise_floor, min_adc_noise_floor, max_adc_noise_floor;
int done_i_adc;
int adc_v_val_prev_prev, adc_v_val_prev, adc_v_val;
int adc_i_val_prev_prev, adc_i_val_prev, adc_i_val;
int isc_stable_adc_v_val = -1;
int isc_stable_adc_v_val_prev = -1;
int isc_stable_adc_v_val_prev_prev = -1;
int isc_stable_adc_i_val = -1;
int isc_stable_adc_i_val_prev = -1;
int isc_stable_adc_i_val_prev_prev = -1;
unsigned long num_meas = 1; // counts IV measurements taken
long start_usecs, elapsed_usecs;
float usecs_per_iv_pair;
#ifdef CAPTURE_UNFILTERED
bool capture_unfiltered = false;
int unfiltered_index = 0;
int unfiltered_adc_v_vals[MAX_UNFILTERED_POINTS];
int unfiltered_adc_i_vals[MAX_UNFILTERED_POINTS];
#endif
// Wait for go (or config) message from host
Serial.println(F("Waiting for go message or config message"));
go_msg_received = false;
while (!go_msg_received) {
if (get_host_msg(incoming_msg)) {
if (strstr_P(incoming_msg, go_str)) {
go_msg_received = true;
}
else if (strstr_P(incoming_msg, config_str)) {
process_config_msg(incoming_msg);
}
}
}
// Measure Vref (indirectly, by measuring bandgap)
read_bandgap(GO_BDGP_READ_ITER);
// Get Voc ADC value and current channel ADC noise floor
voc_adc = 0;
adc_noise_floor = ADC_MAX;
min_adc_noise_floor = ADC_MAX;
max_adc_noise_floor = 0;
memset(adc_v_vals, 0, sizeof(adc_v_vals));
memset(adc_i_vals, 0, sizeof(adc_i_vals));
for (ii = 0; ii < VOC_POLLING_LOOPS; ii++) {
adc_v_val = read_adc(VOLTAGE_CH); // Read voltage channel
adc_i_val = read_adc(CURRENT_CH); // Read current channel
// Update frequency count for this current channel value. We
// temporarily use the adc_v_vals array for the values and the
// adc_i_vals array for the counts
for (index = 0, count_updated = false;
(index < (int)sizeof(adc_v_vals)) && !count_updated;
index++) {
if (adc_i_vals[index] == 0) { // first empty slot
adc_v_vals[index] = adc_v_val;
adc_i_vals[index] = 1; // count
count_updated = true;
} else if (adc_v_vals[index] == adc_v_val) {
adc_i_vals[index]++; // count
count_updated = true;
}
}
// The ADC noise floor is the value read from the ADC when it
// "should" be zero. At this point, we know that the actual current
// is zero because the circuit is open, so whatever value is read on
// the current channel is the noise floor value.
if (adc_i_val < min_adc_noise_floor) {
min_adc_noise_floor = adc_i_val;
}
if (adc_i_val > max_adc_noise_floor) {
max_adc_noise_floor = adc_i_val;
}
}
// The Voc ADC value is the most common value seen during polling
for (index = 0, voc_adc_found = false, max_count = 0;
(index < (int)sizeof(adc_v_vals)) && !voc_adc_found;
index++) {
if (adc_i_vals[index] == 0) {
// When we see a slot with a zero count, we're done
voc_adc_found = true;
} else if (adc_i_vals[index] > max_count) {
voc_adc = adc_v_vals[index];
max_count = adc_i_vals[index];
}
}
adc_noise_floor = min_adc_noise_floor;
// Increase minimum Isc ADC value by noise floor
min_isc_adc += adc_noise_floor;
// Determine the current channel ADC value that indicates the curve
// has reached its tail. This value is twice the noise floor value,
// or 20; whichever is greater.
done_i_adc = adc_noise_floor << 1;
if (done_i_adc < 20) {
done_i_adc = 20;
}
// If Voc is valid, activate relay/SSRs)
//
if (voc_adc < MIN_VOC_ADC) {
// If the Voc ADC value is lower than MIN_VOC_ADC we assume that it
// is actually zero (not connected) and we force it to zero and skip
// the relay activation and Isc polling
skip_isc_poll = true;
voc_adc = 0;
} else {
skip_isc_poll = false;
poll_timeout = true;
// Turn on SSR3 (does nothing if this is not an SSR IVS2 or is a cell
// version that has no SSR3)
digitalWrite(SSR3_PIN, SSR3_ACTIVE);
digitalWrite(FET3_PIN, FET3_ACTIVE);
delay(20); // Let it turn completely on before any current flows
// Activate relay (or SSR1)
digitalWrite(RELAY_PIN, relay_active);
// Turn off SSR2 (does nothing if this is not an SSR IVS2 or is a cell
// version that has no SSR2)
digitalWrite(SSR2_PIN, SSR2_INACTIVE);
}
// Poll for stable Isc
//
adc_v_val_prev_prev = ADC_MAX;
adc_v_val_prev = ADC_MAX;
adc_i_val_prev_prev = 0;
adc_i_val_prev = 0;
for (ii = 0; (ii < max_isc_poll) && !skip_isc_poll; ii++) {
adc_i_val = read_adc(CURRENT_CH); // Read current channel
adc_v_val = read_adc(VOLTAGE_CH); // Read voltage channel
#ifdef CAPTURE_UNFILTERED_ISC_POLL
if (((adc_i_val > min_isc_adc) || capture_unfiltered) &&
(unfiltered_index < MAX_UNFILTERED_POINTS)) {
unfiltered_adc_i_vals[unfiltered_index] = adc_i_val;
unfiltered_adc_v_vals[unfiltered_index++] = adc_v_val;
capture_unfiltered = true;
}
#endif
isc_poll_loops = ii + 1;
if (adc_i_val > min_isc_adc) {
// For the EMR version, Isc is considered stable when three
// consecutive measurements:
// - have current greater than min_isc_adc
// - have increasing voltage
// - have decreasing or equal current
// - have a current difference less than or equal to isc_stable_adc
//
// For the SSR version, Isc is stable when both the voltage and
// current have stopped changing, i.e. three of the same values
// are seen in a row.
//
// Although we don't "know" whether the hardware is an EMR or SSR
// version, it is very unlikely that the EMR conditions would
// match on the SSR hardware or vice versa. But if they do, it
// would most likely not matter.
if (((adc_v_val > adc_v_val_prev) && // EMR conditions
(adc_v_val_prev > adc_v_val_prev_prev) &&
(adc_i_val <= adc_i_val_prev) &&
(adc_i_val_prev <= adc_i_val_prev_prev) &&
(abs(adc_i_val_prev - adc_i_val) <= isc_stable_adc) &&
(abs(adc_i_val_prev_prev - adc_i_val_prev) <= isc_stable_adc))) {
emr_isc_stable = true;
}
if (((adc_v_val == adc_v_val_prev) && // SSR conditions
(adc_v_val_prev == adc_v_val_prev_prev) &&
(adc_i_val == adc_i_val_prev) &&
(adc_i_val_prev == adc_i_val_prev_prev))) {
ssr_isc_stable = true;
}
if (emr_isc_stable || ssr_isc_stable) {
isc_stable_adc_v_val = adc_v_val;
isc_stable_adc_v_val_prev = adc_v_val_prev;
isc_stable_adc_v_val_prev_prev = adc_v_val_prev_prev;
isc_stable_adc_i_val = adc_i_val;
isc_stable_adc_i_val_prev = adc_i_val_prev;
isc_stable_adc_i_val_prev_prev = adc_i_val_prev_prev;
poll_timeout = false;
break;
}
if (adc_v_val >= adc_v_val_prev) {
// If voltage increases or is equal, shift previous to
// previous-previous. But previous-previous keeps its value if
// voltage decreases. This has the effect of discarding the
// previous value, which handles the EMR "bounce" case.
adc_v_val_prev_prev = adc_v_val_prev;
adc_i_val_prev_prev = adc_i_val_prev;
}
// Shift current to previous
adc_v_val_prev = adc_v_val;
adc_i_val_prev = adc_i_val;
}
}
if ((max_isc_poll < 0) && !skip_isc_poll) {
// Special debug case (negative max_isc_poll). Just poll until a
// non-zero current is found
poll_timeout = true;
for (ii = 0; ii < MAX_ISC_POLL; ii++) {
adc_i_val = read_adc(CURRENT_CH); // Read current channel
if (adc_i_val) {
poll_timeout = false;
adc_v_val = read_adc(VOLTAGE_CH); // Read voltage channel
adc_i_val_prev_prev = adc_i_val;
break;
}
}
}
// Turn off SSR3 (SSR4 in cell version) when polling is complete
digitalWrite(SSR3_PIN, SSR3_INACTIVE);
digitalWrite(FET3_PIN, FET3_INACTIVE);
digitalWrite(SSR4_PIN, SSR4_INACTIVE);
if (poll_timeout)
Serial.println(F("Polling for stable Isc timed out"));
// Isc is approximately the value of the first of the three points
// at the end of Isc polling
isc_adc = adc_i_val_prev_prev;
// First IV pair (point number 0) is last point from polling
adc_v_vals[0] = adc_v_val;
adc_i_vals[0] = adc_i_val;
// Get v_scale and i_scale
compute_v_and_i_scale(isc_adc, voc_adc, &v_scale, &i_scale);
// Calculate the minimum scaled adc delta value. This is the Manhattan
// distance between the Isc point and the Voc point divided by the
// maximum number of points. This guarantees that we won't run out of
// memory before the complete curve is captured. However, it will
// usually result in a number of captured points that is a fair amount
// lower than max_iv_points. The max_iv_points value is how many
// points there -would- be if -all- points were the minimum distance
// apart, -and- the the actual distance between the ISC point and the
// VOC point were equal to the Manhattan distance. But some points
// will be farther apart than the minimum distance. One reason is
// simply because, unless max_iv_points is set to a very small number,
// there are portions of the curve where the limiting factor is the
// rate that the measurements can be taken; even without discarding
// measurements, the points are farther apart than the minimum. The
// other reason is that it is unlikely that a measurement comes at
// exactly the minimum distance from the previously recorded
// measurement, so the first one that does satisfy the requirement may
// have overshot the minimum by nearly a factor of 2:1 in the worst
// case. And, of course, the actual IV curve is always shorter than
// the Manhattan distance.
min_manhattan_distance = (unsigned int) ((isc_adc * i_scale) +
(voc_adc * v_scale)) / max_iv_points;
// Proceed to read remaining points on IV curve. Compensate for the
// fact that time passes between I and V measurements by using a
// weighted average for I. Discard points that are not a minimum
// "Manhattan distance" apart (scaled sum of V and I ADC values).
adc_i_val_prev = adc_i_vals[0];
start_usecs = micros();
while (num_meas < MAX_IV_MEAS) {
num_meas++;
//----------------------------------------------------
// Read both channels back-to-back. The current channel is first
// since it was first in the reads for point 0 above.
adc_i_val = read_adc(CURRENT_CH); // Read current channel
adc_v_val = read_adc(VOLTAGE_CH); // Read voltage channel
#ifdef CAPTURE_UNFILTERED_POST_ISC
//------------------------- Unfiltered ----------------------
if (unfiltered_index < MAX_UNFILTERED_POINTS) {
unfiltered_adc_i_vals[unfiltered_index] = adc_i_val;
unfiltered_adc_v_vals[unfiltered_index++] = adc_v_val;
}
#endif
//--------------------- Current channel -----------------
if (update_prev_i) {
// Adjust previous current value to weighted average with this
// value. 16-bit integer math!! Max ADC value is 4095, so no
// overflow as long as sum of I_CH_1ST_WEIGHT and I_CH_2ND_WEIGHT
// is 16 or less.
adc_i_vals[pt_num-1] = (adc_i_val_prev * I_CH_1ST_WEIGHT +
adc_i_val * I_CH_2ND_WEIGHT +
AVG_WEIGHT) / TOTAL_WEIGHT;
}
//--------------------- Voltage channel -----------------
adc_v_vals[pt_num] = adc_v_val;
//------------------------ Deltas -------------------
adc_v_delta = adc_v_val - adc_v_vals[pt_num-1];
adc_v_val_prev = adc_v_val;
adc_i_delta = adc_i_vals[pt_num-1] - adc_i_val;
adc_i_prev_delta = adc_i_val_prev - adc_i_val;
adc_i_val_prev = adc_i_val;
//---------------------- Done check -----------------
// Check if we've reached the tail of the curve.
if (adc_i_val < done_i_adc) {
// Current is very close to zero so we're PROBABLY done
if (adc_i_prev_delta < 3) {
// But only if the current delta is very small
break;
}
}
// We're also done if Isc polling timed out
if (poll_timeout) {
break;
}
//--------------- Voltage decrease check -------------
// At this point we know that all preceding points are in order of
// increasing voltage. However, it is possible that one or more of
// them are erroneously high due to relay "bounce". This is detected
// when the voltage of this point is lower than the voltage of the
// previous point. If that is the case, we search backwards through
// the previous points until we find one that has a lower voltage
// and replace its successor with the current point and rewind the
// pt_num counter. While it is probably not possible for the bounce
// to span more than two or three points, this covers the general
// case of it spanning N points (and starting at any point).
if (adc_v_val < adc_v_vals[pt_num-1]) {
while (pt_num > 1) {
if (adc_v_val < adc_v_vals[pt_num-2]) {
pt_num--;
} else {
break;
}
}
adc_v_vals[pt_num-1] = adc_v_val;
adc_i_vals[pt_num-1] = adc_i_val;
update_prev_i = true; // Adjust this I value on next measurement
continue;
}
//------------------- Discard decision ---------------
// "Manhattan distance" is sum of scaled deltas
manhattan_distance = (adc_v_delta * v_scale) + (adc_i_delta * i_scale);
// Keep measurement if Manhattan distance is big enough; otherwise
// discard. However, if we've discarded max_discards consecutive
// measurements, then keep it anyway.
if ((manhattan_distance >= min_manhattan_distance) ||
(num_discarded_pts >= max_discards)) {
// Keep this one
pt_num++;
update_prev_i = true; // Adjust this I value on next measurement
num_discarded_pts = 0; // Reset discard counter
if (pt_num >= max_iv_points) {
// We're done
break;
}
} else {
// Don't record this one
update_prev_i = false; // And don't adjust prev I val next time
num_discarded_pts++;
}
}
if (update_prev_i) {
// Last one didn't get adjusted (or even saved), so save it now
adc_i_vals[pt_num-1] = adc_i_val;
}
elapsed_usecs = micros() - start_usecs;
// Turn off relay (or SSR1)
digitalWrite(RELAY_PIN, relay_inactive);
// Turn on SSR2 (does nothing if this is not a module version SSR
// IVS2)
digitalWrite(SSR2_PIN, SSR2_ACTIVE);
// Turn on SSR4 (does nothing if this is not a cell version SSR IVS2)
digitalWrite(SSR4_PIN, SSR4_ACTIVE);
// Report results on serial port
//
#ifdef ADS1115_PYRANOMETER_SUPPORTED
int16_t ads1115_val, retries;
long ads1115_val_sum, ads1115_val_avg, ppm_error_from_avg;
bool ads1115_present, tmp36_present, found_stable_value;
// Pyranometer temperature (TMP36)
ads1115.setGain(GAIN_TWO); // -2 V to 2 V
ads1115_val_sum = 0;
ads1115_val_avg = 0;
ads1115_present = true;
tmp36_present = true;
found_stable_value = false;
retries = 0;
while (!found_stable_value && (retries < 20)) {
for (int ii = 0; ii < ADS1115_TEMP_POLLING_LOOPS; ii++) {
ads1115_val = ads1115.readADC_SingleEnded(2);
if (ads1115_val == -1) {
// Value of -1 indicates no ADS1115 is present
ads1115_present = false;
found_stable_value = true;
break;
}
if (ads1115_val < 4000) {
// Values less than 250mV (-25 deg C) are assumed to be noise,
// meaning there is no TMP36 connected to A2
tmp36_present = false;
found_stable_value = true;
break;
}
ads1115_val_sum += ads1115_val;
}
if (ads1115_present && tmp36_present) {
ads1115_val_avg = ads1115_val_sum / ADS1115_TEMP_POLLING_LOOPS;
found_stable_value = true;
ads1115_val_sum = 0;
for (int ii = 0; ii < ADS1115_TEMP_POLLING_LOOPS; ii++) {
ads1115_val = ads1115.readADC_SingleEnded(2);
ppm_error_from_avg =
(1000000 * abs(ads1115_val - ads1115_val_avg)) /
abs(ads1115_val_avg);
if (ppm_error_from_avg > MAX_STABLE_TEMP_ERR_PPM) {
// If any value is more than MAX_STABLE_TEMP_ERR_PPM from the
// average, we don't have a stable value
found_stable_value = false;
retries++;
break;
}
}
}
}
if (ads1115_present && tmp36_present && found_stable_value) {
Serial.print(F("ADS1115 (pyranometer temp sensor) raw value: "));
Serial.println(ads1115_val_avg);
} else if (ads1115_present && tmp36_present) {
Serial.print(F("WARNING: TMP36 pyranometer temp sensor not stable"));
}
// Irradiance (PDB-C139)
if (ads1115_present) {
ads1115.setGain(GAIN_EIGHT); // -512 mV to 512 mV
ads1115_val_sum = 0;
ads1115_val_avg = 0;
found_stable_value = false;
retries = 0;
while (!found_stable_value && (retries < 20)) {
for (int ii = 0; ii < ADS1115_IRRADIANCE_POLLING_LOOPS; ii++) {
ads1115_val = ads1115.readADC_Differential_0_1();
ads1115_val_sum += ads1115_val;
}
ads1115_val_avg = ads1115_val_sum / ADS1115_IRRADIANCE_POLLING_LOOPS;
found_stable_value = true;
ads1115_val_sum = 0;
for (int ii = 0; ii < ADS1115_IRRADIANCE_POLLING_LOOPS; ii++) {
ads1115_val = ads1115.readADC_Differential_0_1();
ppm_error_from_avg =
(1000000 * abs(ads1115_val - ads1115_val_avg)) /
abs(ads1115_val_avg);
if (ppm_error_from_avg > MAX_STABLE_IRRAD_ERR_PPM) {
// If any value is more than MAX_STABLE_IRRAD_ERR_PPM from the
// average, we don't have a stable value
found_stable_value = false;
retries++;
break;
}
}
}
}
if (ads1115_present && found_stable_value) {
Serial.print(F("ADS1115 (pyranometer photodiode) raw value: "));
Serial.println(ads1115_val_avg);
} else if (ads1115_present) {
Serial.println(F("WARNING: pyranometer photodiode not stable"));
}
#endif
#ifdef DS18B20_SUPPORTED
// Temperature
if (num_ds18b20s) {
sensors.requestTemperatures();
for (int ii = 0; ii < num_ds18b20s; ii++) {
Serial.print(F("Temperature at sensor #"));
Serial.print(ii+1);
Serial.print(F(" is "));
Serial.print(sensors.getTempCByIndex(ii));
Serial.println(F(" degrees Celsius"));
}
}
#endif
// CH1 (current channel) ADC noise floor
Serial.print(F("CH1 ADC noise floor (min):"));
Serial.println(min_adc_noise_floor);
Serial.print(F("CH1 ADC noise floor (max):"));
Serial.println(max_adc_noise_floor);
// Isc stable polling
Serial.print(F("EMR Isc stable: "));
Serial.print(emr_isc_stable);
Serial.print(F(" SSR Isc stable: "));
Serial.println(ssr_isc_stable);
if (emr_isc_stable || ssr_isc_stable) {
Serial.print(F("Isc stable point 1: "));
Serial.print(isc_stable_adc_v_val_prev_prev);
Serial.print(F(","));
Serial.println(isc_stable_adc_i_val_prev_prev);
Serial.print(F("Isc stable point 2: "));
Serial.print(isc_stable_adc_v_val_prev);
Serial.print(F(","));
Serial.println(isc_stable_adc_i_val_prev);
Serial.print(F("Isc stable point 3: "));
Serial.print(isc_stable_adc_v_val);
Serial.print(F(","));
Serial.println(isc_stable_adc_i_val);
}
// Isc point
Serial.print(F("Isc CH0:0"));
Serial.print(F(" CH1:"));
Serial.println(isc_adc);
// Middle points
for (ii = 0; ii < pt_num; ii++) {
Serial.print(ii);
Serial.print(F(" CH0:"));
Serial.print(adc_v_vals[ii]);
Serial.print(F(" CH1:"));
Serial.println(adc_i_vals[ii]);
}
// Voc point
Serial.print(F("Voc CH0:"));
Serial.print(voc_adc);
Serial.print(F(" CH1:"));
Serial.println(adc_noise_floor);
#ifdef CAPTURE_UNFILTERED
for (ii = 0; ii < unfiltered_index; ii++) {
Serial.print(ii);
Serial.print(F(" Unfiltered CH0:"));
Serial.print(unfiltered_adc_v_vals[ii]);
Serial.print(F(" Unfiltered CH1:"));
Serial.println(unfiltered_adc_i_vals[ii]);
}
#endif
Serial.print(F("Isc poll loops: "));
Serial.println(isc_poll_loops);
Serial.print(F("Number of measurements: "));
Serial.println(num_meas);
Serial.print(F("Number of recorded points: "));
Serial.println(pt_num);
Serial.print(F("i_scale: "));
Serial.println(i_scale);
Serial.print(F("v_scale: "));
Serial.println(v_scale);
Serial.print(F("min_manhattan_distance: "));
Serial.println(min_manhattan_distance);
Serial.print(F("Elapsed usecs: "));
Serial.println(elapsed_usecs);
usecs_per_iv_pair = (float) elapsed_usecs / num_meas;
Serial.print(F("Time (usecs) per i/v reading: "));
Serial.println(usecs_per_iv_pair);
Serial.println(F("Output complete"));
}
bool get_host_msg(char * msg) {
bool msg_received = false;
char c;
int char_num = 0;
int msg_timer;
msg_timer = MSG_TIMER_TIMEOUT;
while (msg_timer && !msg_received) {
if (Serial.available()) {
c = Serial.read();
if (c == '\n') {
// Substitute NULL for newline
msg[char_num++] = '\0';
msg_received = true;
Serial.print(F("Received host message: "));
Serial.println(msg);
break;
} else {
if (char_num == (MAX_MSG_LEN - 1)) {
msg[char_num] = '\0';
Serial.print(F("ERROR: Host message too long: "));
Serial.print(msg);
Serial.println(F("...."));
break;
} else {
msg[char_num++] = c;
}
}
msg_timer = MSG_TIMER_TIMEOUT;
} else {
msg_timer--;
}
delay(1);
}
return (msg_received);
}
void process_config_msg(char * msg) {
char *substr = NULL;
char *config_type = NULL;
char *config_val = NULL;
char *config_val2 = NULL;
int ii = 0;
int num_args = 0;
int exp_args;
int eeprom_addr;
int count, adc_v_val, adc_i_val;
float eeprom_value;
bool wrong_arg_cnt = false;
const char CARRIAGE_RETURN = 0xd;
substr = strtok(msg, " "); // "Config:"
while (substr != NULL) {
substr = strtok(NULL, " ");
if (substr != NULL &&
*substr != CARRIAGE_RETURN) { // Windows phenomenon
if (ii == 0) {
config_type = substr;
} else if (ii == 1) {
config_val = substr;
num_args = 1;
} else if (ii == 2) {
config_val2 = substr;
num_args = 2;
} else if (substr != NULL) {
Serial.println(F("ERROR: Too many fields in config message"));
Serial.println(F("Config not processed"));
return;
}
}
ii++;
}
if (strcmp_P(config_type, clk_div_str) == 0) {
exp_args = 1;
if (num_args == exp_args) {
clk_div = atoi(config_val);
SPI.setClockDivider(clk_div);
} else {
wrong_arg_cnt = true;
}
} else if (strcmp_P(config_type, max_iv_points_str) == 0) {
exp_args = 1;
if (num_args == exp_args) {
max_iv_points = atoi(config_val);
if (max_iv_points > MAX_IV_POINTS) {
max_iv_points = MAX_IV_POINTS;
}
} else {
wrong_arg_cnt = true;
}
} else if (strcmp_P(config_type, min_isc_adc_str) == 0) {
exp_args = 1;
if (num_args == exp_args) {
min_isc_adc = atoi(config_val);
} else {
wrong_arg_cnt = true;
}
} else if (strcmp_P(config_type, max_isc_poll_str) == 0) {
exp_args = 1;
if (num_args == exp_args) {
max_isc_poll = atoi(config_val);
} else {
wrong_arg_cnt = true;
}
} else if (strcmp_P(config_type, isc_stable_adc_str) == 0) {
exp_args = 1;
if (num_args == exp_args) {
isc_stable_adc = atoi(config_val);
} else {
wrong_arg_cnt = true;
}
} else if (strcmp_P(config_type, max_discards_str) == 0) {
exp_args = 1;
if (num_args == exp_args) {
max_discards = atoi(config_val);
} else {
wrong_arg_cnt = true;
}
} else if (strcmp_P(config_type, aspect_height_str) == 0) {
exp_args = 1;
if (num_args == exp_args) {
aspect_height = atoi(config_val);
} else {
wrong_arg_cnt = true;
}
} else if (strcmp_P(config_type, aspect_width_str) == 0) {
exp_args = 1;
if (num_args == exp_args) {
aspect_width = atoi(config_val);
} else {
wrong_arg_cnt = true;
}
} else if (strcmp_P(config_type, write_eeprom_str) == 0) {
exp_args = 2;
if (num_args == exp_args) {
eeprom_addr = atoi(config_val);
eeprom_value = atof(config_val2);
EEPROM.put(eeprom_addr, eeprom_value);
if (eeprom_addr == EEPROM_RELAY_ACTIVE_HIGH_ADDR) {
relay_active = (eeprom_value == LOW) ? LOW : HIGH;
relay_inactive = (relay_active == LOW) ? HIGH : LOW;
digitalWrite(RELAY_PIN, relay_inactive);
digitalWrite(SECOND_RELAY_PIN, relay_inactive);
}
} else {
wrong_arg_cnt = true;
}
} else if (strcmp_P(config_type, dump_eeprom_str) == 0) {
exp_args = 0;
if (num_args == exp_args) {
dump_eeprom();
} else {
wrong_arg_cnt = true;
}
} else if (strcmp_P(config_type, relay_state_str) == 0) {
exp_args = 1;
if (num_args == exp_args) {
set_relay_state((bool)atoi(config_val));
} else {
wrong_arg_cnt = true;
}
} else if (strcmp_P(config_type, second_relay_state_str) == 0) {
exp_args = 1;
if (num_args == exp_args) {
set_second_relay_state((bool)atoi(config_val));
} else {
wrong_arg_cnt = true;
}
} else if (strcmp_P(config_type, do_ssr_curr_cal_str) == 0) {
exp_args = 0;
if (num_args == exp_args) {
do_ssr_curr_cal();
} else {
wrong_arg_cnt = true;
}
} else if (strcmp_P(config_type, read_bandgap_str) == 0) {
exp_args = 0;
if (num_args == exp_args) {
read_bandgap(CMD_BDGP_READ_ITER);
} else {
wrong_arg_cnt = true;
}
} else if (strcmp_P(config_type, read_adc_str) == 0) {
exp_args = 1;
if (num_args == exp_args) {
count = atoi(config_val);
for (ii = 0; ii < count; ii++) {
adc_v_val = read_adc(VOLTAGE_CH); // Read voltage channel
adc_i_val = read_adc(CURRENT_CH); // Read current channel
Serial.print(F("ADC CH0 (voltage): "));
Serial.print(adc_v_val);
Serial.print(F(" CH1 (current): "));
Serial.println(adc_i_val);
delay(500); // 0.5 seconds between reads
}
} else {
wrong_arg_cnt = true;
}
} else {
Serial.print(F("ERROR: Unknown config type: "));
Serial.println(config_type);
Serial.println(F("Config not processed"));
return;
}
if (wrong_arg_cnt) {
Serial.print(F("ERROR: Expected "));
Serial.print(exp_args);
Serial.print(F(" args for config type "));
Serial.print(config_type);
Serial.print(F(", got "));
Serial.println(num_args);
Serial.println(F("Config not processed"));
return;
}
Serial.println(F("Config processed"));
return;
}
void dump_eeprom() {
int eeprom_addr, eeprom_valid_count;
float eeprom_value;
// Dump valid EEPROM entries
EEPROM.get(0, eeprom_value);
// Only dump if address 0 has "magic" value
if (eeprom_value == EEPROM_VALID_VALUE) {
// Second location has count of valid entries
EEPROM.get(sizeof(float), eeprom_value);
eeprom_valid_count = (int)eeprom_value;
for (eeprom_addr = 0;
eeprom_addr < ((eeprom_valid_count + 2) * (int)sizeof(float));
eeprom_addr += sizeof(float)) {
EEPROM.get(eeprom_addr, eeprom_value);
Serial.print(F("EEPROM addr: "));
Serial.print(eeprom_addr, DEC);
Serial.print(F(" value: "));
Serial.println(eeprom_value, 4);
}
}
}
char get_relay_active_val() {
// The IV Swinger 2 hardware design calls for an active-low triggered
// relay module. Support has been added now for the alternate use of
// an active-high triggered relay module. The host software writes
// EEPROM address 44 with either the value 0 or 1 indicating
// active-low or active-high repectively. At the beginning of setup()
// this function is called to determine which type the relay is. It
// is possible that EEPROM has not been written yet, or that it was
// written by an older version of the host software and does not have
// a valid value at address 44. In either of these cases, the default
// value of LOW is returned.
int eeprom_valid_count;
float eeprom_value;
// Check that address 0 has "magic" value
EEPROM.get(0, eeprom_value);
if (eeprom_value == EEPROM_VALID_VALUE) {
// Second location has count of valid entries
EEPROM.get(sizeof(float), eeprom_value);
eeprom_valid_count = (int)eeprom_value;
// Check that EEPROM contains an entry for the relay active value
if ((eeprom_valid_count + 1) * sizeof(float) >=
EEPROM_RELAY_ACTIVE_HIGH_ADDR) {
EEPROM.get(EEPROM_RELAY_ACTIVE_HIGH_ADDR, eeprom_value);
if (eeprom_value == 0) {
return (LOW);
} else {
return (HIGH);
}
} else {
// If EEPROM is not programmed with the relay active value, we
// have to assume it is active-low
return (LOW);
}
} else {
// If EEPROM is not programmed (at all), we have to assume the relay
// is active-low
return (LOW);
}
}
void set_relay_state(bool active) {
if (active) {
digitalWrite(RELAY_PIN, relay_active);
} else {
digitalWrite(RELAY_PIN, relay_inactive);
}
}
void set_second_relay_state(bool active) {
if (active) {
digitalWrite(SSR6_PIN, SSR6_INACTIVE);
digitalWrite(SECOND_RELAY_PIN, relay_active);
} else {
digitalWrite(SECOND_RELAY_PIN, relay_inactive);
digitalWrite(SSR6_PIN, SSR6_ACTIVE);
}
}
void do_ssr_curr_cal() {
bool result_valid = true;
int adc_i_val;
bool keep_going;
long adc_i_val_sum, adc_i_val_p1_avg, adc_i_val_avg_cnt;
float adc_i_val_p2_avg;
long start_usecs, elapsed_usecs;
// Measure Vref (indirectly, by measuring bandgap)
read_bandgap(CMD_BDGP_READ_ITER);
// Activate SSR3/4
digitalWrite(SSR3_PIN, SSR3_ACTIVE); // module version
digitalWrite(FET3_PIN, FET3_ACTIVE); // module version
digitalWrite(SSR4_PIN, SSR4_ACTIVE); // cell version
// Deactivate SSR2
digitalWrite(SSR2_PIN, SSR2_INACTIVE); // module version
// Activate SSR1
digitalWrite(RELAY_PIN, relay_active);
// Loop for SSR_CAL_USECS microseconds
//
// This period is long enough for a human to read the measured value
// on the DMM display.
//
// At the end of the loop, there are two periods of SSR_CAL_RD_USECS
// in which the ADC current channel is read in a loop and the average
// ADC is calculated. If the difference between the Pass 1 average and
// Pass 2 average is more than 1%, the measurement is considered
// "unstable".
//
start_usecs = micros();
elapsed_usecs = 0;
// Pre-Pass 1
//
// Just spin waiting for the elapsed time to reach 2 x
// SSR_CAL_RD_USECS from the end (i.e. the beginning of Pass 1)
//
while (elapsed_usecs < (SSR_CAL_USECS - 2 * SSR_CAL_RD_USECS)) {
elapsed_usecs = micros() - start_usecs;
}
//
// Pass 1
//
// Loop until SSR_CAL_RD_USECS from the end. Accumulate sum of ADC
// values and number of reads (for average ADC calculation). Bail out
// if ADC saturated is seen.
//
// Pass 2
//
// Loop the rest of the way doing the same.
//
for (int pass = 1; pass <= 2; pass++) {
adc_i_val_sum = 0;
adc_i_val_avg_cnt = 0;
keep_going = true;
while (keep_going) {
adc_i_val = read_adc(CURRENT_CH); // Read current channel
adc_i_val_sum += adc_i_val;
adc_i_val_avg_cnt++;
if (adc_i_val > (ADC_SAT - 10))
result_valid = false;
elapsed_usecs = micros() - start_usecs;
if (pass == 1) {
keep_going = ((elapsed_usecs < (SSR_CAL_USECS - SSR_CAL_RD_USECS)) &&
result_valid);
} else {
keep_going = ((elapsed_usecs < SSR_CAL_USECS) && result_valid);
}
}
if (pass == 1) {
// Compute the Pass 1 average
adc_i_val_p1_avg = adc_i_val_avg_cnt ?
adc_i_val_sum / adc_i_val_avg_cnt : 0;
}
}
//
// If the result is valid so far (ADC not saturated), compute the Pass
// 2 average and check that it is within 1% of the Pass 1 average.
// Print message with stability determination and both averages. Note
// that Pass 1 average is integer (long) and Pass 2 average is
// floating point at this point.
//
if (result_valid) {
// Compute the Pass 2 average
adc_i_val_p2_avg = adc_i_val_avg_cnt ?
float(adc_i_val_sum) / float(adc_i_val_avg_cnt) : 0.0;
if (abs(adc_i_val_p2_avg - adc_i_val_p1_avg)/adc_i_val_p2_avg > 0.01) {
Serial.print(F("SSR current calibration ADC not stable. Pass 1: "));
result_valid = false;
} else {
Serial.print(F("SSR current calibration ADC stable. Pass 1: "));
}
Serial.print(adc_i_val_p1_avg);
Serial.print(F(" Pass 2: "));
Serial.println(adc_i_val_p2_avg);
} else {
Serial.println(F("SSR current calibration: ADC saturated"));
}
// Deactivate SSR1
digitalWrite(RELAY_PIN, relay_inactive);
// Activate SSR2
digitalWrite(SSR2_PIN, SSR2_ACTIVE);
// Deactivate SSR3/4
digitalWrite(SSR3_PIN, SSR3_INACTIVE);
digitalWrite(FET3_PIN, FET3_INACTIVE);
digitalWrite(SSR4_PIN, SSR4_INACTIVE);
// If the result is valid, print result (average ADC value)
if (result_valid) {
Serial.print(F("SSR current calibration ADC value: "));
// Round Pass 2 average to nearest integer
Serial.println(int(adc_i_val_p2_avg + 0.5));
}
}
void set_up_bandgap() {
analogReference(DEFAULT);
// Set the reference to Vcc and the measurement to the internal 1.1V bandgap
ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
delay(2); // Wait for Vref to settle
}
void read_bandgap(int iterations) {
long result = 0;
read_internal_adc();
for (long ii = 0; ii < iterations; ii++) {
result += (long)read_internal_adc();
}
Serial.print(F("Bandgap total ADC: "));
Serial.print(result);
Serial.print(F(" iterations: "));
Serial.println(iterations);
}
int read_internal_adc() {
// Read the Arduino internal ADC
ADCSRA |= _BV(ADSC); // Start conversion
while (bit_is_set(ADCSRA, ADSC)); // measuring
return ADC; // ADCH, ADCL
}
int read_adc(int ch) {
// This code assumes MCP3202. MCP3302 would be slightly different.
int ms_byte, ls_byte, cmd_bytes;
cmd_bytes = (ch == 0) ?
B10100000 : // SGL/~DIFF=1, CH=0, MSBF=1
B11100000; // SGL/~DIFF=1, CH=1, MSBF=1
digitalWrite(ADC_CS_PIN, CS_ACTIVE); // Assert active-low chip select
SPI.transfer (B00000001); // START=1
ms_byte = SPI.transfer(cmd_bytes); // Send command, get result
ms_byte &= B00001111; // Bits 11:8 (mask others)
ls_byte = SPI.transfer(0x00); // Bits 7:0
digitalWrite(ADC_CS_PIN, CS_INACTIVE); // Deassert active-low chip select
return ((ms_byte << 8) | ls_byte); // {ms_byte, lsb}
}
void compute_v_and_i_scale(int isc_adc, int voc_adc,
int * v_scale, int * i_scale) {
// Find integer scaling values for V and I, with the sum of the values
// being 16 or less. These values are used for calculating the
// "Manhattan distance" between points when making the discard
// decision. The idea is that the criterion for the minimum distance
// between points on a horizontal part of the curve should be equal to
// the criterion for the minimum distance between points on a vertical
// part of the curve. The distance is literally the spacing on the
// graph. The distance between points on a diagonal part of the curve
// is overcounted somewhat, but that results in slightly closer points
// near the knee(s) of the curve, and that is good. The two factors
// that determine the distance are:
//
// - The maximum ADC value of the axis
// - The aspect ratio of the graph
//
// The maximum value on the X-axis (voltage) is the Voc ADC value.
// The maximum value on the Y-axis (current) is the Isc ADC value.
// Since the graphs are rendered in a rectangular aspect ratio, the
// scale of the axes differs. The initial scaling values could be:
//
// initial_v_scale = aspect_width / voc_adc;
// initial_i_scale = aspect_height / isc_adc;
//
// That would require large values for aspect_width and aspect_height
// to use integer math. Instead, proportional (but much larger) values
// can be computed with:
//
// initial_v_scale = aspect_width * isc_adc;
// initial_i_scale = aspect_height * voc_adc;
//
// An algorithm is then performed to reduce the values proportionally
// such that the sum of the values is 16 or less.
//
// This function is only run once, but speed is important, so 16-bit
// integer math is used exclusively (no floats or longs).
//
bool i_scale_gt_v_scale;
int initial_v_scale, initial_i_scale;
int lg, sm;
int round_up_mask = 0;
int lg_scale, sm_scale;
char bit_num, shift_amt = 0;
initial_v_scale = aspect_width * isc_adc;
initial_i_scale = aspect_height * voc_adc;
i_scale_gt_v_scale = initial_i_scale > initial_v_scale;
lg = i_scale_gt_v_scale ? initial_i_scale : initial_v_scale;
sm = i_scale_gt_v_scale ? initial_v_scale : initial_i_scale;
// Find leftmost bit that is set in the larger initial value. The
// right shift amount is three less than this bit number (to result in
// a 4-bit value, i.e. 15 or less). Also look at the highest bit that
// will be shifted off, to see if we should round up by adding one to
// the resulting shifted amount. If we get all the way down to bit 4
// and it isn't set, the initial values will be used as-is.
for (bit_num = 15; bit_num >= 4; bit_num--) {
if (lg & (1 << bit_num)) {
shift_amt = bit_num - 3;
round_up_mask = (1 << (bit_num - 4));
break;
}
}
// Shift, and increment shifted amount if rounding up is needed
lg_scale = (lg & round_up_mask) ? (lg >> shift_amt) + 1 : (lg >> shift_amt);
sm_scale = (sm & round_up_mask) ? (sm >> shift_amt) + 1 : (sm >> shift_amt);
// If the sum of these values is greater than 16, divide them both by
// two (no rounding up here)
if (lg_scale + sm_scale > 16) {
lg_scale >>= 1;
sm_scale >>= 1;
}
// Make sure sm_scale is at least 1 (necessary?)
if (sm_scale == 0) {
sm_scale = 1;
if (lg_scale == 16)
lg_scale = 15;
}
// Return values at pointer locations
*v_scale = i_scale_gt_v_scale ? sm_scale : lg_scale;
*i_scale = i_scale_gt_v_scale ? lg_scale : sm_scale;
}