# How to convert readings from a DIY capacitive liquid level sensor to a level?

### Background

I have made a tubular capacitive liquid level sensor (This is a professional one, for example). A 555 circuit outputs a frequency that is inversely proportional to the capacitance. This is read into the microcontroller (Arduino Nano clone).

The sensor will be used for monitoring our water tanks that supply our house.
There will be this one for the tank that gets municipal water, and another one for the tanks that collect rain water.

It is communicated with (and powered) over a Cat5 cable acting as a Modbus trunk.

The sensor consists of a DIY probe, made of aluminum and PVC pipes, and a custom board (made with stripboard). The main components of the custom board are:

• Arduino Nano
• NE555
• ISL81487LIPZ (MAX485 replacement; DigiKey)
• DC-DC buck-converter

### The problem

I have taken readings and put them into an Excel spreadsheet, along with the depth of water when they were taken. They seem to be logarithmic, but they are not. I have taken the natural logarithm and normalized the values, and plotted this versus the percentage of submersion of the sensor.

This is the graph:

X is percentage submerged, Y is reading. "Log Normalized" is the plot of the normalized logarithm values. "Linear" is the plot of the wanted values. "Diff" is the difference between the "Log Normalized" value and the "Linear" value for each pair of those values.

In the final application, I will have a reading, and will need to find the depth of the water the sensor is in.

Summed up,
I need a way to translate a reading to a level.

I need it it to be

1. easy to implement (I am not a master mathematician ;) )
2. work with 32-bit floating point numbers
3. fast

The reason it needs to be fast is because of other compensation algorithms that will need to be added, e.g. when the water level drops, the reading doesn't change immediately, it changes as the probe dries. These algorithms will need to call the level calculating one quite often.

### What I Am Using at the Moment

I am currently using the formula described in my answer.
It gives acceptable accuracy (for the purpose of the sensor), is simple, is fast, and just "feels right".

### Other Stuff

The spreadsheet is here. Note: I am still actively using it. There are formulas in place (which my answer here is based on), and some other experiments off to the side.

Images of the Fritzing Breadboard, the electronics, and are here.
The Fritzing file is here.
(I couldn't get Fritzing to comply with editing the schematic. It crashed whenever I moved more than one component at a time, or any large component.)

Cross-posted on math.stackexchange.com.

• Why are you expecting a perfectly log response? As long as you know the curve, you can produce a look up table to linearise it? Feb 8 at 19:14
• I can get a pretty accurate trendline in Excel with a cubic equation on the original readings ($R^2=0.99979$). Fairly accurate too with just a quadratic (0.9939). I'd lean toward using the quadratic since cubic may be overfitting. Feb 8 at 19:18
• Justin did same as I , depending on acceptable tolerance error 2nd (1.2%) or 3rd order (0.1%) i.stack.imgur.com/3lvMN.png Feb 8 at 19:24
• Darryl, Have you considered the idea that it's almost entirely one type of dielectric (air?) at one end of $x$ and almost entirely the other type of dielectric (whatever?) at the other end of $x$ and that right in the middle it's 50:50? There are fringing fields involved and my gut tells me that what you are experiencing may be expected. I haven't sat myself down to work this before, so I'm not drawing upon any specific developed intuition. But I wouldn't have been shocked to see this kind of data.
– jonk
Feb 8 at 20:39
• It is quite possible that the frequency produced is inversely proportional to the square root of the capacitance. However, we don't have a schematic, so we can't tell. Feb 8 at 21:46

You can use a cubic in 1000/reading:

static const float coeff[4]= { 1.62914270704482, -7.9851592788591,  38.5529790479781, -23.7806334509969};

//  Evaluate using Horner's Rule
result = coeff[0];
for (int i=1; i<4; i++)
result = result*x + coeff[i];


I see a maximum error of +0.41/-0.58% of span at the given points and evaluation takes 1 divide and 3 multiplications so it should be pretty fast even on an 8-bit micro. Coefficients were optimized to minimize sum of squares of errors rather than worst absolute error.

Interested to see the comparison with your transcendental functions cos/log.

• This is very interesting. I am probably going to use one of the formulas I showed in my answer, since I am going to make more of these sensors, and a single formula that may not need any tweaking, or a formula with just a single constant that needs tweaking, would be best. However, if one of them doesn't work out, due to accuracy or speed, this will definitely be my next approach. I have edited my question to explain why I need speed. Feb 10 at 14:22

I have found a satisfactory formula/algorithm for my purposes. It gets the maximum error to around 5% (max: 5.02%, mean: 2.49%), which is quite possibly measurement error. While less accurate than other methods, I find it more simple and "idiomatic."

1. Let $$\f\$$ be the frequency reading.

2. Let $$\r\$$ be the normalized log of $$\f\$$, using predefined minimum ($$\l\$$) and maximum ($$\h\$$) expected values. $$r = {\ln f - \ln l \over \ln h - \ln l}$$

3. Let $$\R\$$ be the final result. Then $$R = r + \sin {r\pi \over 2}$$

Below is a simple implementation of the formula in C++. It is based on my actual code, and should compile fine with PlatformIO. (Note: In reality, this is packaged in a class, in a custom library. If anyone wants the code, ask in the comments and I will put it on GitHub.)

/*
* NOTE: Arduino Nano doesn't have 64-bit floating point numbers, but I have used
* 'double's anyway, so that platforms that do support them can gain some accuracy.
*
* reading, and therefore min and max as well, is an integer.
* (because that's how the FreqCount library gives it to you).
*/

#include <Arduino.h>

#include <FreqCount.h>

// minimum expected frequency: the frequency when the water is highest/deepest.
const uint32_t min = 423;
// maximum expected frequency: the frequency when the water is lowest/shallowest.
// 1440 is just a value I picked on the high end. I will be adjusting it.
const uint32_t max = 1440;

const double min_log = log(min);
const double max_log = log(max);

double calculate_level(uint32_t value)
{
// Take the logarithm of the value and normalize it
// (adjust it to be between 0 and 1, inclusive).
double reading =  (log(value) - min_log) / (max_log - min_log);

// Apply the formula.

// Subtract from 1, because the values get higher as there is less water,
// and we want the opposite.
return 1.0 - result;
}

void setup()
{
Serial.begin(9600);

// Init FreqCount to measure the number of pulses per 1000 ms.
FreqCount.begin(1000);
}

void loop()
{
if (FreqCount.available())
{
double level = calculate_level(FreqCount.read()); // See NOTE
Serial.println(level, 2);
}
}



This is the resulting graph, using the formula above:

NOTE: I profiled my actual implementation reading the frequency from FreqCount and calculating the level (equivalent to the line that has a comment saying "See NOTE"). It takes, on average, 344 microseconds. That's fast enough for me :).

This is trick. Capacitive level sensing works but needs to be calibrated for, like, everything. It seems that your output is referred to the circuit you are using.

Capacitance of a level sensor (well, capacitance of anything) is proportional to the area covered and to the dielectric coefficient. Here Physics: LibreTexts: 8.2: Capacitors and Capacitance

You have the usual physical explanation and some horrible curve integrals that derive the capacitance of the cylinder plate capacitor, which your sensor should be a kind of. That's of course in the ideal world.

Since your capacitor should be cylindrical (is it really? if there is skew or taper the geometry change and we are talking about sub-pF differences), capacitance should be dependant only to the cylinder length and dielectric. In total you should have, in parallel:

• The capacitance of the length in the fluid, plus
• The capacitance of the length in air, plus
• Some somewhat fixed capacity due to cabling and other things;

Also you will have some other capacitance to ground, depending on the material of your container. And shielding is useful too, a guard conductor helps against charge leakage (remember that commercial capacitance converter resolution is specified in femtoFarads).

Then you have the environment. This is fully fluid dependant. Often temperature dependant.

That said, it seems that something is quite wrong with your converter or transducer. It should really be a linear relationship between level and capacitance (often the air section capacitance is negligible).

For example look at TIDA-00317: Capacitive-Based Liquid Level Sensing Sensor Reference Design

The corresponding part is quite advanced but the application notes have many ideas on how to design the sensor itself (and the issue on the process)

• The dielectric constant of fluid introduces the error. as the two dielectrics are in series and both changing with level. e.g. $\epsilon _R=80$ for water Feb 8 at 19:41
• Why in series? in a level sensor the air and fluid capacitor parts are in parallel Feb 8 at 19:44
• Yes could be in parallel if electrodes are parallel to the fluid Feb 8 at 19:45
• If it's a coaxial probe then it looks like the air capacitor in parallel with the fluid capacitor to me, @Tony. What have you got in mind? Feb 8 at 20:59