Давно хотел попробовать воспользоваться «интервальной арифметикой» для вычислений с учётом паспортных погрешностей датчиков, кварцев и другой аппаратуры.
Интервальная арифметика определена стандартом IEEE 1788-2015 - IEEE Standard for Interval Arithmetic, но там реальные, зубодробительные дебри. Настолько реальные, что через два года вышел «облегчённый» стандарт 1788.1-2017 - IEEE Standard for Interval Arithmetic (Simplified), но и в нём в разы больше всякой фигни, чем нам с Вами нужно.
Если кому-то интересна библиотека, полностью реализующая IEEE 1788-2015, их есть у нас, пожалуйста (под Ардуино сами адаптируйте). Я же для своего эксперимента написал сегодня на коленке микро-реализацию интервальной арифметики (только нужные мне операции, но они правильные - по стандарту) и решил поэкспериментировать с нею, а именно, написать программку измерения расстояния датчиком HC-SR04 с температурной компенсацией (по формуле Лапласа). При этом предлагалось учитывать погрешности кварца, датчика расстояния, датчика температуры и погрешность, вносимую особенностями функции pulseIn
.
Вот собсна библиотека
#ifndef TINY1788_H
#define TINY1788_H
// Базовый тип хранения чисел
// TNumber может быть: float, double или long double
typedef double TNumber;
//
// Нигде и никак не првоеряется корректность операций
// (деление на 0, извлечение корня из отрицатльеного числа и т.п.)
// после операция можно проверить всё ли нормально методом isValid()
//
class Tiny1788 : public Printable{
public:
Tiny1788(const TNumber f = 0.0, const TNumber s = 0.0) : m_first(f), m_second(s) {
normalize();
}
Tiny1788 & setValue(const TNumber val, const TNumber precission) {
m_first = val - precission;
m_second = val + precission;
normalize(); // дуракоустойчивость а вдруг precission < 0
return * this;
}
bool isValid(void) const {
return
isfinite(m_first) &&
isfinite(m_second) &&
m_first <= m_second;
}
TNumber wid(void) const { return m_second - m_first; }
TNumber mid(void) { return (m_second + m_first) / 2.0; }
TNumber rad(void) { return wid() / 2.0; }
TNumber lower(void) const { return m_first; }
TNumber upper(void) const { return m_second; }
Tiny1788 & operator += (const Tiny1788 &a) { return *this = *this + a; }
Tiny1788 & operator -= (const Tiny1788 &a) { return *this = *this - a; }
Tiny1788 & operator *= (const Tiny1788 &a) { return *this = *this * a; }
Tiny1788 & operator /= (const Tiny1788 &a) { return *this = *this / a; }
Tiny1788 & operator += (const TNumber a) { return *this = *this + a; }
Tiny1788 & operator -= (const TNumber a) { return *this = *this - a; }
Tiny1788 & operator *= (const TNumber a) { return *this = *this * a; }
Tiny1788 & operator /= (const TNumber a) { return *this = *this / a; }
size_t printTo(Print & p) const { // в качестве p нам передадут, например, Serial
const double lg = log10(wid());
int pos = lg < 0 ? 1 - static_cast<int>(floor(lg)): 2;
if (pos > 7) pos = 7;
size_t res = p.print('[');
res += p.print(m_first, pos);
res += p.print(',');
res += p.print(m_second, pos);
return res + p.print(']');
}
private:
TNumber m_first, m_second;
void normalize(void) {
if (m_first > m_second) {
const TNumber c = m_first;
m_first = m_second;
m_second = c;
}
}
static TNumber fmax4(const TNumber a, const TNumber b, const TNumber c, const TNumber d) { return fmax(fmax(a, b) , fmax(c, d)); }
static TNumber fmin4(const TNumber a, const TNumber b, const TNumber c, const TNumber d) { return fmin(fmin(a, b) , fmin(c, d)); }
friend Tiny1788 operator + (const Tiny1788 &, const Tiny1788 &);
friend Tiny1788 operator - (const Tiny1788 &, const Tiny1788 &);
friend Tiny1788 operator * (const Tiny1788 &, const Tiny1788 &);
friend Tiny1788 operator / (const Tiny1788 &, const Tiny1788 &);
friend Tiny1788 operator + (const Tiny1788 &, const TNumber);
friend Tiny1788 operator - (const Tiny1788 &, const TNumber);
friend Tiny1788 operator - (const TNumber, const Tiny1788 &);
friend Tiny1788 operator * (const Tiny1788 &, const TNumber);
friend Tiny1788 operator / (const Tiny1788 &, const TNumber);
};
//Сложение: [a,b] + [c,d] = [a + c, b + d]
inline Tiny1788 operator + (const Tiny1788 &a, const Tiny1788 &b) {
return Tiny1788(a.lower() + b.lower(), a.upper() + b.upper());
}
//Вычитание: [a,b] − [c,d] = [a − d, b − c]
inline Tiny1788 operator - (const Tiny1788 &a, const Tiny1788 &b) {
return Tiny1788(a.lower() - b.upper(), a.upper() + b.lower());
}
//Умножение: [a,b] × [c,d] = [min (ac, ad, bc, bd), max (ac, ad, bc, bd)]
inline Tiny1788 operator * (const Tiny1788 &a, const Tiny1788 &b) {
const TNumber ac = a.lower() * b.lower();
const TNumber ad = a.lower() * b.upper();
const TNumber bc = a.upper() * b.lower();
const TNumber bd = a.upper() * b.upper();
return Tiny1788(Tiny1788::fmin4(ac, ad, bc, bd), Tiny1788::fmax4(ac, ad, bc, bd));
}
// После деления, результат надо проверять на isValid
// т.к. в делителе могут быть нули и здесь это не проверяется
//Деление: [a,b] / [c,d] = [min (a/c, a/d, b/c, b/d), max (a/c, a/d, b/c, b/d)]
inline Tiny1788 operator / (const Tiny1788 &a, const Tiny1788 &b) {
const TNumber ac = a.lower() / b.lower();
const TNumber ad = a.lower() / b.upper();
const TNumber bc = a.upper() / b.lower();
const TNumber bd = a.upper() / b.upper();
return Tiny1788(Tiny1788::fmin4(ac, ad, bc, bd), Tiny1788::fmax4(ac, ad, bc, bd));
}
inline Tiny1788 operator + (const Tiny1788 &a, const TNumber b) { return a + Tiny1788(b, b); }
inline Tiny1788 operator + (const TNumber a, const Tiny1788 &b) { return b + a; }
inline Tiny1788 operator - (const Tiny1788 &a, const TNumber b) { return a - Tiny1788(b, b); }
inline Tiny1788 operator - (const TNumber b, const Tiny1788 &a) { return Tiny1788(b, b) - a; }
inline Tiny1788 operator * (const Tiny1788 &a, const TNumber b) { return a * Tiny1788(b, b); }
inline Tiny1788 operator * (const TNumber a, const Tiny1788 &b) { return b * a; }
inline Tiny1788 operator / (const Tiny1788 &a, const TNumber b) { return a / Tiny1788(b, b); }
inline Tiny1788 operator / (const TNumber b, const Tiny1788 &a) { return Tiny1788(b, b) / a; }
inline Tiny1788 sqrt(const Tiny1788 &a) { return Tiny1788(sqrt(a.lower()), sqrt(a.upper())); }
#endif // TINY1788_H
А вот программа, с её использованием. Места, где я учитываю погрешности, я прокомментировал. Все вычисления ведутся над интервалами (тип Tiny1788
). Результатом тоже является интервал.
Программа
#define printVar(x) do { Serial.print(#x); Serial.print('='); Serial.println(x); } while (false)
#include <OneWire.h>
#include <DallasTemperature.h>
#include "Tiny1788.h"
constexpr uint8_t triggerPin = 2;
constexpr uint8_t theEchoPin = 3;
constexpr uint8_t oneWireBus = 4;
constexpr double quarzAccuracy = 20e-6; // погрешность кварца в ppm (с потолка, но разумная для кварцев без термокомпенсации)
const Tiny1788 ds18b20Accuracy(-0.5, 0.5); // погрешность DS18B20 из даташита
const Tiny1788 HCSR04Accuracy(-0.3, 0.3); // погрешность HC-SR04 из даташита
OneWire oneWire(oneWireBus);
DallasTemperature sensors(&oneWire);
// Возвращает длительность в микросекундах в виде интервала "от и до"
Tiny1788 getDuration(void) {
digitalWrite(triggerPin, LOW);
delayMicroseconds(2);
digitalWrite(triggerPin, HIGH);
delayMicroseconds(10);
digitalWrite(triggerPin, LOW);
double res = pulseIn(theEchoPin, HIGH);
//
// Поскольку pulseIn выдаёт целые микросекунды, считаем,
// что это даёт погрешность плюс/минус полмикросекунды.
// также здесь учитываем погрешность кварца
// В результате возвращаем интервал
return Tiny1788((1 - quarzAccuracy) * (res - 0.5), (1 + quarzAccuracy) * (res + 0.5));
}
// возвращает температуру в градусах Цельсия в виде интервала "от и до"
Tiny1788 getTemperature(void) {
sensors.requestTemperatures();
return ds18b20Accuracy + sensors.getTempCByIndex(0);
}
void setup(void) {
pinMode(theEchoPin, INPUT);
pinMode(triggerPin, OUTPUT);
Serial.begin(9600);
sensors.begin();
sensors.setResolution(12);
Tiny1788 temperature = getTemperature();
Tiny1788 duration = getDuration();
const Tiny1788 absTemp = 273.15 + temperature; // абсолютная температура
// Здесь считается скорость звука по формуле Лапласа
// умножение на 1e-4 - для перевода из м/с в см/мкс
const Tiny1788 c = 20.05 * sqrt(absTemp) * 1e-4; // скорость звука в сантиметрах в микросекунду
// Считаем расстояния и прибавляем к нему паспортную погрешность HC-SR04
const Tiny1788 distance = c * duration / 2 + HCSR04Accuracy; // расстояние
printVar(distance); // печатаем.
}
void loop(void) {}
Ну, что тщательно выставил расстояние до преграды - 20 см.
Результат
distance=[19.49,20.14]
попробовал с другими расстояниями (до 3 м). Реальное расстояние всегда оказывалось внутри интервала - результата. Мне понравилось.
Пока не осознал, нужно ли мне это. Но, в принципе, если хочется знать поточнее, то правильный путь - такой, а не считать со 100500 знаками после запятой, когда погрешность датчика ±0,5. Здесь получается правильная точность, а не карнавал бессмысленных цифирек после запятой.