Еще раз про интервальную арифметику

Как-то была тема про интервальную арифметику. Мне показалось, что не все поняли для чего это нужно. Вот решил привести пример использования этой техники, который возник у меня совершенно реально в реальной деятельности.

Кстати, в тексте библиотеки, что там приведён, есть глупая опечатка (плюс вместо минуса в одном месте), но поправить я не могу. Жаль. На старом форуме я мог менять свои темы когда угодно, и даже сейчас могу. А тут – лежит программа с глупой ошибкой и ничего поделать нельзя :frowning:

Вот свежий текст библиотеки. В нём исправлена та ошибка, а также внесено несколько новых добавлен ещё один конструктор для удобства задания 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 стоило бы в четыре раза дороже – меня, в своё время, жаба задушила.

И ещё у меня есть

Просьба к электронщикам

Обе приведённые схемы (с фильтром, и с гистерезисом) я собирал в железе и они работают. Но, я нутром чую, что компаратор (а он же ведь по сути – операционный усилитель, только специализированный) как-то, где-то может самовозбуждаться и где-то в этих схемах не помешал бы конденсатор.

Был бы весьма признателен за ликбез, куда его надо всунуть и как (хотя бы в общих чертах) посчитать его номинал.

Спасибо.

5 лайков

Чтобы избежать этого, специально придумали потенциометр.
Лично я обычно пользуюсь такой схемой:
image
Где номинал R2 выбирается чуть большим, чем максимальная погрешность резисторов R1 и R3. Я бы поставил в данном случае 330-470 Ом.

Да, это первое, что приходит в голову.
Но здесь есть один неупомянутый, но существенный недостаток: в схему вносится запаздывание, определяемое постоянной времени R3C1 (по Вашей схеме). То есть фронты “синего” сигнала на осциллограмме уйдут вправо. Думаю, если изменить масштаб развертки осциллографа по времени, это станет заметным.
И мне кажется, это гораздо более существенный недостаток, чем необходимость договариваться с жабой.

Не задам (из вредности). Я сам всегда поступаю точно так же. И, кроме того, у меня складывается впечатление (подкрепленное результатами измерений), что китайцы широко используют ряд E24 для 1%-х резисторов, и именно такими и торгуют.

Я, правда, не электронщик, но по поводу самовозбуждения могу сказать следующее:

  1. Насколько мне известно, самовозбуждение может случаться в линейном режиме. В режиме компаратора коэффициент усиления примерно равен 0, поэтому самовозбуждению взяться неоткуда.
  2. Если ОУ требует внешней емкости, то, как правило, она подключается к отдельным специально для этого предназначенным выводам, и берется не из расчета, а из дэйташита.
  3. “2” не исключает необходимости в отдельных случаях применения емкости. В этом случае она включается между выходом и инвертирующим входом. При ее расчете фигурирует сопротивление параллельно включенных R5 и R4. Суть в том, чтобы на частоте, где сдвиг фазы достигнет 180 градусов, коэффициент усиления был меньше 1 (ОУ сам по себе ФНЧ*). Но это все только для линейного режима, в режиме компаратора - не актуально.
  4. И последнее: по моему опыту, чтобы исключить возможность самовозбуждения “на ровном месте”, нужно установить керамический конденсатор порядка 0.1 как можно ближе к выводам питания ОУ.

Примечание* : Порядка выше первого. Емкостью мы снижаем частоту единичного усиления, добываясь, чтобы на этой частоте суммарный порядок ФНЧ был меньше второго.

1 лайк

Спасибо!

1 лайк

Я так понимаю, что постоянная составляющая нас не интересует. Тогда можно её отсечь конденсатором, а далее импульсы амплитудой 0.12 В прогнать через инвертирующий усилитель с Ку= 30-50. На входе можно добавить RC фильтр.
Схема с компаратором очень чувствительна к постоянной составляющей сигнала.

1 лайк

А я думаю наоборот, Ку ничем не ограничен и его значение максимально. Да и Ку=0 означает, что при любом значении входного сигнала на выходе будет 0 В. Или имелось ввиду что-то другое?

Ну, статья-то про интервальную арифметику, а при таком подходе она не нужна :slight_smile:

Как это?
Ограничен напряжением питания.

Отнюдь.
Означает, что при изменении разницы между входами напряжение на выходе не изменяется. А оно и не может измениться, т.к. уперлось в “рельсу”.

Вот в такой схеме при среднем положении потенциометра усиление ОУ равно ноль.
Питание двуполярное.