Как-то была тема про интервальную арифметику. Мне показалось, что не все поняли для чего это нужно. Вот решил привести пример использования этой техники, который возник у меня совершенно реально в реальной деятельности.
Кстати, в тексте библиотеки, что там приведён, есть глупая опечатка (плюс вместо минуса в одном месте), но поправить я не могу. Жаль. На старом форуме я мог менять свои темы когда угодно, и даже сейчас могу. А тут – лежит программа с глупой ошибкой и ничего поделать нельзя ![]()
Вот свежий текст библиотеки. В нём исправлена та ошибка, а также внесено несколько новых добавлен ещё один конструктор для удобства задания N-процентных резисторов и немного изменена печать интервала, мне показалось так удобнее.
Текст файла 'Tiny1788.h'
#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();
}
//
// Конструктор для значения c +/- p %
// Последний параметр нужен только для того, чтобы этот конструктор
// синтаксически отличался от предыдущего. Можно передать что угодно,
// например, '%' для красоты
Tiny1788(const TNumber c, const TNumber p, const char perc) :
m_first(c * (1.0 - p / 100.0)),
m_second(c * (1.0 + p / 100.0)) {
((void)perc); // чтобы не ругался, что не используем
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) const { return (m_second + m_first) / 2.0; }
TNumber rad(void) const { 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(mid(), pos);
res += p.print(" +/- ");
res += p.print(rad(), pos);
res += p.print("; ");
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; }
// Никак не проверяется случай отрицательных аргументов sqrt
inline Tiny1788 sqrt(const Tiny1788 &a) {
return Tiny1788(sqrt(a.lower()), sqrt(a.upper()));
}
#endif // TINY1788_H
Итак, возникла вот такая задача. В готовом устройстве имеется сигнал (меандр) частотой около 1 Герца. Верхнее напряжение 1,16 В, нижнее 1,04 В. Сигнал сильно зашумлён, но шум, слава Богу, регулярный – просто прёт синусоида с частотой 5 кГц и амплитудой 50 мВ. Вот такой, примерно, сигнал:
Питание устройства 5 В, причём точно 5В, сколько ни замерял.
Требуется превратить верхние участки сигнала в LOW, а нижние – в HIGH (логика 5-вольтовая).
Ну, что, в идеальном мире, в котором принцессы не какают, а розовые пони питаются звуками музыки, можно было бы сделать простой инвертирующий триггер Шмитта с напряжением переключения “посерёдке” – 1.1В (красная линия). Примерно, вот так:
И в этом самом идеальном мире, эта схема даже работала бы (по крайней мере протеус в этом со мною согласен)
Всё бы хорошо, только вот, как пел Владимир Семёныч: «Жаль, сады сторожат». Живём-то мы в реальном мире. А в реальном мире идеально точных резисторов не бывает!
Берём одно-процентные и посчитаем что мы на них можем получить в наихудшем случае. Считать будем делитель напряжения R1-R2. Вот программа:
#define printVar(x) do { Serial.print(#x); Serial.print('='); Serial.println(x); } while (false)
#include <Tiny1788.h>
static constexpr TNumber Vcc = 5.00; // (В) Напряжение питания
static constexpr TNumber resistorAccuracy = 1.0; // в процентах
static const Tiny1788 R1 (39000, resistorAccuracy, '%'); // 39 кОм, 1%
static const Tiny1788 R2 (11000, resistorAccuracy, '%'); // 11 кОм, 1%
void setup(void) {
Serial.begin(9600);
Serial.println("Let's go on!");
Tiny1788 Vin = Vcc * R2 / (R1 + R2);
printVar(Vin);
}
void loop(void) {}
///////////////////////////////
//
// Результат
//
// Let's go on!
// Vin=1.100 +/- 0.022; [1.078,1.122]
В строках №№ 8 и 9 заданы резисторы, а в строке №14 до боли знакомая формула делителя напряжения. Результат приведён в комментарии. Как видите, имеем разброс от средней точки 1,1 В на ±22 милливольта, т.е. при однопроцентных резисторах, напряжение у нас может получиться от 1,078 В до 1,122 В – как повезёт. Нанесём эти границы на рисунок сигнала (красные пунктирные линии):
Т.е., если нам не повезёт, то напряжение может “залезть” на шум и наш триггер будет переключаться как сумасшедший. Кстати, мне повезло и схема с рис. 2 заработала у меня в железе с первого раза. Но могло и не повезти – пришлось бы долго и нудно подбирать экземпляры резисторов. Лучше избегать таких схем.
И что делать? Ну, в принципе, здесь, конечно, есть
очень простое решение
Наш шум, слава Богу, вполне регулярный и его легко отфильтровать и, если сделать это достаточно жёстко (уменьшить пульсации до “незаметности”), то, как видно из рисунка, 4, там будет достаточно большой запас (40 мВ в каждую сторону) и всё должно работать надёжно. Добавляем ФНЧ (R3, C1) в схему с большим запасом по частоте:
Нормально работает (а куда ей деваться-то?) В железе тоже нормально работает.
но мы не ищем лёгких путей сейчас изучаем применение интервальных вычислений, потому рассмотрим другое решение (которое, кстати, ещё и дешевле – экономит целый конденсатор – это можно будет передать Жабе).
Другое решение – это сделать триггер с гистерезисом, выбрав, в качестве порогов переключения, средние линии нашего сигнала (1,04 В и 1,16 В). Для этого достаточно добавить в схему с рис. 2 один резистор и всё пересчитать. Получается вот такая схема.
В симуляторе (с точными резисторами) она отлично работает:
Теперь, проверим куда могут попасть наши пороги переключения за счёт того, что резисторы допускают 1% погрешности. Вот программа:
#define printVar(x) do { Serial.print(#x); Serial.print('='); Serial.println(x); } while (false)
#include <Tiny1788.h>
static constexpr TNumber Vcc = 5.00; // (В) Напряжение питания
static constexpr TNumber Voh = 5.0; // (В) на деле, чуть меньше. Надо вычесть падение на
// Rpu от тока утечки выходного транзистора компаратора (приведён в
// даташите), но там копейки, которые ничего не меняют
static constexpr TNumber Vol = 0.03; // (В) LOW на выходе при 1mA через Rpu (из даташита)
static constexpr TNumber resistorAccuracy = 1.0; // в процентах
static const Tiny1788 R1 (100000, resistorAccuracy, '%');
static const Tiny1788 R2 ( 27000, resistorAccuracy, '%');
static const Tiny1788 R3 (820000, resistorAccuracy, '%');
static const Tiny1788 Rpu ( 4700, resistorAccuracy, '%');
void setup(void) {
Serial.begin(9600);
Serial.println("Let's go on!");
// Пороги срабатывания триггера Шмитта. Верхний и нижний
Tiny1788 Vh = R2 * (Vcc * (R3 + Rpu) + R1 * Voh) / ((R1 + R2) * (R3 + Rpu) + R2 * R1);
Tiny1788 Vl = R2 * (Vcc * R3 + R1 * Vol) / (R2 * (R3 + R1) + R1 * R3);
//
printVar(Vl);
printVar(Vh);
}
void loop(void) {}
///////////////////////////////
//
// Результат
//
// Let's go on!
// Vl=1.038 +/- 0.041; [0.996,1.079]
// Vh=1.163 +/- 0.046; [1.116,1.209]
В строках №№ 11-15 заданы наши три резистора и pull-up резистор. В строке №21 считается верхний порог переключения, а в строке №22 – нижний. В результате имеем: верхний порог, при любом, даже самом неудачном, раскладе с номиналами резисторов, будет находиться между 1,116 В и 1,209 В, а нижний – между 0,996 В и 1,079 В. Нанесём эти границы пунктирными линиями соответствующих цветов на наш сигнал:
Как видно, все возможные значения пороговых напряжений находятся в допустимых пределах и всё должно работать нормально. Оно и работает.
Ну, вот, примерно так и можно использовать интервальные вычисления. Кстати, если Вы в любой из двух приведённых программ замените resistorAccuracy с 1 на 5 и пересчитаете, то убедитесь, что с 5%-ыми резисторами в этой задаче ловить нечего (при предлагаемых схемах). Повезти, конечно, может, но именно повезти.
Ответ на вопрос Andriano (всё равно ведь задаст)
Я считаю 1% резисторы на номинальный ряд Е24 (а не E96), потому что у меня всегда есть под рукой резисторы всех 24 номиналов этого ряда и всех кратностей от единиц ом до десятков мегаом. Когда какого-то номинала остаётся меньше двадцати, я покупаю ещё сотню и так уже много лет – они у меня всегда есть. Поддерживать в таком состоянии ряд E96 стоило бы в четыре раза дороже – меня, в своё время, жаба задушила.
И ещё у меня есть
Просьба к электронщикам
Обе приведённые схемы (с фильтром, и с гистерезисом) я собирал в железе и они работают. Но, я нутром чую, что компаратор (а он же ведь по сути – операционный усилитель, только специализированный) как-то, где-то может самовозбуждаться и где-то в этих схемах не помешал бы конденсатор.
Был бы весьма признателен за ликбез, куда его надо всунуть и как (хотя бы в общих чертах) посчитать его номинал.
Спасибо.










