Implement ADC correction for the soil sensor for a range of input voltages
This commit is contained in:
parent
5b3c6a471d
commit
fb48ad7da7
4 changed files with 67 additions and 29 deletions
|
|
@ -23,6 +23,37 @@ constexpr double kBattDividerR1 = 1470;
|
||||||
constexpr double kBattDividerR2 = 470;
|
constexpr double kBattDividerR2 = 470;
|
||||||
constexpr double kBattDividerFactor =
|
constexpr double kBattDividerFactor =
|
||||||
(kBattDividerR1 + kBattDividerR2) / kBattDividerR2;
|
(kBattDividerR1 + kBattDividerR2) / kBattDividerR2;
|
||||||
|
|
||||||
|
constexpr double kDiodeDrop = 0.6;
|
||||||
|
|
||||||
|
// We need the sensor to behave well as the battery discharge. I plan on
|
||||||
|
// using a CR2032 coin cell, whose voltage ranges from roughly 3.0 (charged)
|
||||||
|
// to 2.0V (discharged). This is the input range for Vdd, so all of our
|
||||||
|
// analog-to-digital (ADC) measurements are somewhat relative to this varying
|
||||||
|
// voltage. The soil sensor itself is at heart an RC circuit: it charges to
|
||||||
|
// 0.63Vcc within a time constant (RC). More importantly, within a cycle, the
|
||||||
|
// final charge is proportional to Vcc. If we sample this with our ADC, and use
|
||||||
|
// a sampling mode that is relative to Vcc (AR_VDD4 here), the final voltage
|
||||||
|
// should "cancel out", keeping the sampled value constant as we vary Vcc. In
|
||||||
|
// other words, in theory, the raw value returned from the ADC should remain
|
||||||
|
// constant as I keep the sensor in the same state and vary the input voltage.
|
||||||
|
// In practice, I'm seeing a small drift in the raw sampled values. This could
|
||||||
|
// be due to some capacitance or lower transistor response times in the fast
|
||||||
|
// discharge circuit.
|
||||||
|
// I captured some (Vcc, raw adc sample) pairs and did a linear regression to
|
||||||
|
// correct for these values when the sensor is out in the air and in the water.
|
||||||
|
// This should mitigate the drifting issue enough.
|
||||||
|
// In reality, I don't think leaving this correction out would be a big deal. I
|
||||||
|
// expect the battery to discharge over a period of a few months, and I'd expect
|
||||||
|
// plants to be watered with the period of days/couple of weeks. This drift
|
||||||
|
// wouldn't be noticeable in that time scale, but since we can do better, why
|
||||||
|
// not?
|
||||||
|
// Some assumptions for these hardcoded values:
|
||||||
|
// - Sampling resolution of 10 bits!
|
||||||
|
// - Input (vdd) is in the range 2.0 - 3.0V
|
||||||
|
double GetRawValAir(double vdd) { return vdd * 94.29 + 308.95; }
|
||||||
|
double GetRawValWater(double vdd) { return vdd * 36.02 - 57.1; }
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
double BatteryMonitor::Read() {
|
double BatteryMonitor::Read() {
|
||||||
|
|
@ -33,15 +64,22 @@ double BatteryMonitor::Read() {
|
||||||
return kBattDividerFactor * v_in;
|
return kBattDividerFactor * v_in;
|
||||||
}
|
}
|
||||||
|
|
||||||
soil_reading_t SoilMonitor::Read() {
|
soil_reading_t SoilMonitor::Read(double vdd) {
|
||||||
analogOversampling(kOversampling);
|
analogOversampling(kOversampling);
|
||||||
// Set up the analog reference to be VDD. This allows us to cancel out
|
// Set up the analog reference to be VDD. This allows us to cancel out
|
||||||
// the effect of the battery discharge across time, since the RC circuit
|
// the effect of the battery discharge across time, since the RC circuit
|
||||||
// also depends linearly on VDD.
|
// also depends linearly on VDD.
|
||||||
analogReference(AR_VDD4);
|
analogReference(AR_VDD4);
|
||||||
int raw = analogRead(pin_);
|
int raw = analogRead(pin_);
|
||||||
double percentage =
|
double v_out_corr = (vdd * raw) / 1023 + 0.6;
|
||||||
static_cast<double>(raw - air_val_) / (water_val_ - air_val_);
|
int raw_corr = v_out_corr / vdd * 1023;
|
||||||
|
|
||||||
|
double air = GetRawValAir(vdd);
|
||||||
|
double water = GetRawValWater(vdd);
|
||||||
|
double percentage = static_cast<double>(raw - air) / (water - air);
|
||||||
|
// Serial.printf(
|
||||||
|
// "vdd: %f, raw: %d, raw_corr: %d, v_out_corr: %f, percentage: %f\n", vdd,
|
||||||
|
// raw, raw_corr, v_out_corr, 100 * percentage);
|
||||||
return {raw, std::max(0.0, std::min(1.0, percentage))};
|
return {raw, std::max(0.0, std::min(1.0, percentage))};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,15 +19,11 @@ struct soil_reading_t {
|
||||||
|
|
||||||
class SoilMonitor {
|
class SoilMonitor {
|
||||||
public:
|
public:
|
||||||
explicit SoilMonitor(int air_val, int water_val, int pin)
|
explicit SoilMonitor(int pin) : pin_(pin) {}
|
||||||
: air_val_(air_val), water_val_(water_val), pin_(pin) {}
|
|
||||||
|
|
||||||
soil_reading_t Read();
|
soil_reading_t Read(double vdd);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Referencce values for estimating the soil moisture percentage.
|
|
||||||
const int air_val_, water_val_;
|
|
||||||
|
|
||||||
// Pin the analog signal is connected to.
|
// Pin the analog signal is connected to.
|
||||||
const int pin_;
|
const int pin_;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,6 @@ constexpr int kSoilAnalogPin = 21; // P0.31, AIN7
|
||||||
constexpr int kBattAnalogPin = 15; // P0.05, AIN3
|
constexpr int kBattAnalogPin = 15; // P0.05, AIN3
|
||||||
constexpr int kDischargeEnablePin = 16; // P0.30;
|
constexpr int kDischargeEnablePin = 16; // P0.30;
|
||||||
constexpr double kPWMFrequency = 500000;
|
constexpr double kPWMFrequency = 500000;
|
||||||
constexpr int kSoilMonitorAirVal = 680;
|
|
||||||
constexpr int kSoilMonitorWaterVal = 60;
|
|
||||||
|
|
||||||
// parasite::SquareWaveGenerator square_wave_generator(kPWMFrequency, kPWMPin);
|
// parasite::SquareWaveGenerator square_wave_generator(kPWMFrequency, kPWMPin);
|
||||||
|
|
||||||
|
|
@ -24,8 +22,7 @@ const parasite::MACAddr kMACAddr = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
|
||||||
parasite::BLEAdvertiser advertiser(kMACAddr);
|
parasite::BLEAdvertiser advertiser(kMACAddr);
|
||||||
|
|
||||||
parasite::BatteryMonitor batt_monitor(kBattAnalogPin);
|
parasite::BatteryMonitor batt_monitor(kBattAnalogPin);
|
||||||
parasite::SoilMonitor soil_monitor(kSoilMonitorAirVal, kSoilMonitorWaterVal,
|
parasite::SoilMonitor soil_monitor(kSoilAnalogPin);
|
||||||
kSoilAnalogPin);
|
|
||||||
|
|
||||||
SoftwareTimer timer;
|
SoftwareTimer timer;
|
||||||
|
|
||||||
|
|
@ -54,25 +51,24 @@ void updateAdvertisingData(parasite::BLEAdvertiser* advertiser,
|
||||||
void timer_cb(TimerHandle_t timer_handle) {
|
void timer_cb(TimerHandle_t timer_handle) {
|
||||||
parasite::SquareWaveGenerator square_wave_generator(kPWMFrequency, kPWMPin);
|
parasite::SquareWaveGenerator square_wave_generator(kPWMFrequency, kPWMPin);
|
||||||
|
|
||||||
digitalToggle(kLED1Pin);
|
// digitalToggle(kLED1Pin);
|
||||||
|
|
||||||
square_wave_generator.Start();
|
|
||||||
digitalWrite(kDischargeEnablePin, HIGH);
|
|
||||||
|
|
||||||
parasite::soil_reading_t soil_reading = soil_monitor.Read();
|
|
||||||
// Serial.printf("Moisture val: %d, %f%%\n", soil_reading.raw,
|
|
||||||
// 100 * soil_reading.parcent);
|
|
||||||
|
|
||||||
square_wave_generator.Stop();
|
|
||||||
digitalWrite(kDischargeEnablePin, LOW);
|
|
||||||
|
|
||||||
double battery_voltage = batt_monitor.Read();
|
double battery_voltage = batt_monitor.Read();
|
||||||
// Serial.printf("Batt voltage: %f\n", battery_voltage);
|
Serial.printf("Batt voltage: %f\n", battery_voltage);
|
||||||
|
|
||||||
|
digitalWrite(kDischargeEnablePin, HIGH);
|
||||||
|
square_wave_generator.Start();
|
||||||
|
delay(10);
|
||||||
|
parasite::soil_reading_t soil_reading = soil_monitor.Read(battery_voltage);
|
||||||
|
Serial.printf("Moisture val: %d, %f%%\n", soil_reading.raw,
|
||||||
|
100 * soil_reading.parcent);
|
||||||
|
square_wave_generator.Stop();
|
||||||
|
digitalWrite(kDischargeEnablePin, LOW);
|
||||||
|
|
||||||
updateAdvertisingData(&advertiser, soil_reading, battery_voltage);
|
updateAdvertisingData(&advertiser, soil_reading, battery_voltage);
|
||||||
|
|
||||||
// Keep adversiting for 1 second.
|
// Keep adversiting for 1 second.
|
||||||
delay(1000);
|
delay(500);
|
||||||
|
|
||||||
advertiser.Stop();
|
advertiser.Stop();
|
||||||
|
|
||||||
|
|
@ -81,10 +77,14 @@ void timer_cb(TimerHandle_t timer_handle) {
|
||||||
|
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(9600);
|
Serial.begin(9600);
|
||||||
pinMode(kLED1Pin, OUTPUT);
|
// pinMode(kLED1Pin, OUTPUT);
|
||||||
pinMode(kDischargeEnablePin, OUTPUT);
|
pinMode(kDischargeEnablePin, OUTPUT);
|
||||||
|
|
||||||
timer.begin(2000, timer_cb, /*timerID=*/nullptr, /*repeating=*/true);
|
// digitalWrite(kDischargeEnablePin, HIGH);
|
||||||
|
// parasite::SquareWaveGenerator square_wave_generator(kPWMFrequency,
|
||||||
|
// kPWMPin); square_wave_generator.Start();
|
||||||
|
|
||||||
|
timer.begin(1000, timer_cb, /*timerID=*/nullptr, /*repeating=*/true);
|
||||||
timer.start();
|
timer.start();
|
||||||
|
|
||||||
// Suspend the loop task. Under the hood this is a freeRTOS task set up
|
// Suspend the loop task. Under the hood this is a freeRTOS task set up
|
||||||
|
|
|
||||||
|
|
@ -140,9 +140,13 @@ Vcc = 2.0 => Moisture Air: 547; Hand: 23
|
||||||
|
|
||||||
Possible issues:
|
Possible issues:
|
||||||
* Fast discharging circuit is not fully open with lower voltage?
|
* Fast discharging circuit is not fully open with lower voltage?
|
||||||
|
* Weird capacitance stored in the transitor messes up the quick switching?
|
||||||
|
* Time it takes to overcome the 0.6V diode drop in the output circuit?
|
||||||
|
|
||||||
Possible fixes:
|
Possible fixes:
|
||||||
* Hack a software interpolation
|
* Measure and correct it via a software interpolation
|
||||||
|
|
||||||
|
For now I'm implementing the software fix that seems to work well in practice. The measurements and regression coefficients are in [this spreadsheet](https://docs.google.com/spreadsheets/d/1F6rtkCX7626tzZff0NANZVknawb_U4uS38zjyy6ZPEE/edit#gid=1442651809).
|
||||||
|
|
||||||
# Battery
|
# Battery
|
||||||
* [CR2032 datasheet](https://data.energizer.com/pdfs/cr2032.pdf)
|
* [CR2032 datasheet](https://data.energizer.com/pdfs/cr2032.pdf)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue