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 kBattDividerFactor =
|
||||
(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
|
||||
|
||||
double BatteryMonitor::Read() {
|
||||
|
|
@ -33,15 +64,22 @@ double BatteryMonitor::Read() {
|
|||
return kBattDividerFactor * v_in;
|
||||
}
|
||||
|
||||
soil_reading_t SoilMonitor::Read() {
|
||||
soil_reading_t SoilMonitor::Read(double vdd) {
|
||||
analogOversampling(kOversampling);
|
||||
// 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
|
||||
// also depends linearly on VDD.
|
||||
analogReference(AR_VDD4);
|
||||
int raw = analogRead(pin_);
|
||||
double percentage =
|
||||
static_cast<double>(raw - air_val_) / (water_val_ - air_val_);
|
||||
double v_out_corr = (vdd * raw) / 1023 + 0.6;
|
||||
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))};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,15 +19,11 @@ struct soil_reading_t {
|
|||
|
||||
class SoilMonitor {
|
||||
public:
|
||||
explicit SoilMonitor(int air_val, int water_val, int pin)
|
||||
: air_val_(air_val), water_val_(water_val), pin_(pin) {}
|
||||
explicit SoilMonitor(int pin) : pin_(pin) {}
|
||||
|
||||
soil_reading_t Read();
|
||||
soil_reading_t Read(double vdd);
|
||||
|
||||
private:
|
||||
// Referencce values for estimating the soil moisture percentage.
|
||||
const int air_val_, water_val_;
|
||||
|
||||
// Pin the analog signal is connected to.
|
||||
const int pin_;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,8 +15,6 @@ constexpr int kSoilAnalogPin = 21; // P0.31, AIN7
|
|||
constexpr int kBattAnalogPin = 15; // P0.05, AIN3
|
||||
constexpr int kDischargeEnablePin = 16; // P0.30;
|
||||
constexpr double kPWMFrequency = 500000;
|
||||
constexpr int kSoilMonitorAirVal = 680;
|
||||
constexpr int kSoilMonitorWaterVal = 60;
|
||||
|
||||
// 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::BatteryMonitor batt_monitor(kBattAnalogPin);
|
||||
parasite::SoilMonitor soil_monitor(kSoilMonitorAirVal, kSoilMonitorWaterVal,
|
||||
kSoilAnalogPin);
|
||||
parasite::SoilMonitor soil_monitor(kSoilAnalogPin);
|
||||
|
||||
SoftwareTimer timer;
|
||||
|
||||
|
|
@ -54,25 +51,24 @@ void updateAdvertisingData(parasite::BLEAdvertiser* advertiser,
|
|||
void timer_cb(TimerHandle_t timer_handle) {
|
||||
parasite::SquareWaveGenerator square_wave_generator(kPWMFrequency, kPWMPin);
|
||||
|
||||
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);
|
||||
// digitalToggle(kLED1Pin);
|
||||
|
||||
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);
|
||||
|
||||
// Keep adversiting for 1 second.
|
||||
delay(1000);
|
||||
delay(500);
|
||||
|
||||
advertiser.Stop();
|
||||
|
||||
|
|
@ -81,10 +77,14 @@ void timer_cb(TimerHandle_t timer_handle) {
|
|||
|
||||
void setup() {
|
||||
Serial.begin(9600);
|
||||
pinMode(kLED1Pin, OUTPUT);
|
||||
// pinMode(kLED1Pin, 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();
|
||||
|
||||
// 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:
|
||||
* 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:
|
||||
* 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
|
||||
* [CR2032 datasheet](https://data.energizer.com/pdfs/cr2032.pdf)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue