1. Введение
2. Весы и софт
3. Шумы ненагруженных весов
4. Чувствительность и задержки
5. Релаксация и гистерезис
6. Выводы
7. Дополнение
1. Введение
Несколько простых "прикидочных" экспериментов с тензовесами, которые будут описаны ниже, я делал исключительно для себя, ориентируясь на свои задачи. Но потом подумал, что эти результаты могут оказаться интересны еще кому-нибудь. Поэтому я решил оформить их в "презентабельном" виде и опубликовать здесь.
Электронные весы, имеющие какой-нибудь открытый интерфейс с компьютером - очень полезная штука в экспериментальном "хозяйстве". В Сети описано много разных вариантов их использования и в наших задачах. Меня они заинтересовали, в первую очередь, как датчик скорости отбора. В процессах автоматизированной ректификации такой датчик не очень-то и важен, т.к. отбор контролируется отдельным контроллером со специальным регулятором расхода продукта (электромагнитный клапан, перистальтический насос и т.п.). Обычно контроллер отбора не только регулирует скорость отбора, но и подсчитывает суммарный расход жидкости, прошедшей через устройство отбора. Тем не менее, возможны нештатные (аварийные) ситуации, которые контроллер устройства отбора отследить не в состоянии. Например, соринка заткнула жиклер клапана, окончательно износился и стал протекать кляп клапана, порвалась трубка перистальтического насоса и т.п. Для таких ситуаций в системе должен быть предусмотрен независимый датчик скорости отбора. Роль такого датчика как раз и могли бы выполнить электронные весы с компьютерным интерфейсом, которые сигнализировали бы систем о существенных отклонениях реальной скорости отбора от устанавливаемых контроллером отбора.
В этой задаче (создание "аварийного" датчика скорости отбора) есть упрощающие и усложняющие факторы. Упрощающими факторами является, во-первых, невысокая требуемая точность измерений. Во-вторых, при ректификации всегда отбирается почти чистый (точнее - неразбавленный) спирт с относительно небольшим содержанием других примесей. Поэтому никакой коррекции на плотность жидкости вводить не нужно. Т.е. измерение объема, с точностью до температурного фактора, эквивалентно измерению массы. Усложняющие факторы тоже есть. Во-первых, процессы ректификации обычно длительные (десятки часов), а скорости отбора относительно невелики (десятки мл/час при отборе голов и сотни мл/час при отборе других фракций). Т.е. дрейф весов под нагрузкой должен быть в разумных пределах. Во-вторых, тензодатчики должны длительное время "держать на себе" довольно приличный вес. Для средних ("бытовых":) кубов в 20-30 л это вес порядка 10-15 кг. А для второй ректификации он может быть еще больше. Точность весов с таким предельным весом, заявляемая производителем, обычно составляет величину порядка 1 гр. А поскольку скорость отбора - это производная веса по времени, то требования к точности весов и минимальным шумам становятся доминирующими и входят в серьезное противоречие с требованием большого предельного веса используемых весов.
Теоретически разбираться с этой дилеммой и глубоко влезать в метрологические проблемы тензовесов мне совсем не хотелось. Все-таки это лишь вспомогательное устройство в моей автоматике. Да и некогда. Поэтому я решил, что проще купить подходящие весы, переоборудовать их и проверить экспериментально годятся они для такой задачи или нет. При любом исходе, весы с компьютерным интерфейсом в хозяйстве никогда не помешают :)
2. Весы и софт
Про весы на ардуинках в Сети написано так много, что можно ограничиться лишь краткими комментариями по специфичным вопросам. В качестве основы я взял симпатичные и недорогие платформенные весы VT-8004 от фирмы Vitek. У них 4 сенсора, которые расположены на каждой из четырех ножек. Платформа стеклянная и круглая. Вполне устойчивая конструкция для 10-кг весов. Всю родную начинку (кроме, естественно, самих сенсоров) я выкинул и соединил сенсоры по мостовой схеме. Порывшись по сусекам, нашел платку с АЦП HX711, купленную давно для экспериментов с тензодатчиками. Простейший вариант для работы с АЦП и передачи данных в компьютер - любая ардуинка с нормальным USB. Из маленьких ардунок с USB у меня под рукой оказалась RF Nano. По сути это та же Arduino Nano, только с радиомодулем RF24L01 на борту. Радиомодуль для данной задачи не нужен, но другой ардуинки (с нормальным USB) под рукой не оказалось. Потом поставлю что-нибудь попроще. Все эти компоненты я соединил как положено и получил неплохие электронные USB-весы, вид которых показан на рисунках ниже.
Небольшой комментарий по поводу HX711. Этот АЦП может работать на двух частотах, определяемых уровнем линии RATE (15-я ножка микросхемы). При низком уровне на линии RATE частота работы АЦП составляет 10 Гц, при высоком уровне - 80 Гц. На многих платах с микросхемой HX711 эта ножка глухо посажена на землю. Т.е. по умолчанию установлена частота работы АЦП 10 Гц. Нам нужно 80. В этом случае 15-ю ножку микросхемы (линия RATE) нужно аккуратненько отпаять, отогнуть и соединить с соседней (c 16-й) ножкой тоненьким проводком. На 16-ю ножку на плате подается цифровое питание. После этого АЦП будет оцифровывать входной сигнал с частотой 80 Гц.
Теперь рассмотрим кратко программное обеспечение. В рассматриваемой системе есть два программируемых узла. Первый - плата Arduino, которая обеспечивает непосредственное взаимодействие с АЦП HX711, предварительную обработку и усреднение выборок АЦП (80 выборок в течение 1 сек). Второй программируемый узел - это компьютер (хост), который получает данные от ардуинки, производит (если нужно) дополнительную низкочастотную фильтрацию (усреднение) и обеспечивает интерфейс с пользователем. Это может быть любой компьютер с USB-портом. В принципе, это может быть и смартфон с USB. Я же использовал микрокомпьютер Raspberry Pi 4 (малинка) с HDMI монитором, беспроводной клавиатурой и мышкой. Можно работать и с "голой" малинкой по SSH с другого компьютера или телефона.
Текст скетча для Arduino очень прост:
Скрытый текст
#include "HX711.h"
const int DOUT_PIN = 7;
const int SCK_PIN = 6;
HX711 scale;
void setup() {
Serial.begin(57600);
scale.begin(DOUT_PIN, SCK_PIN);
}
void loop() {
if(scale.is_ready()) {
long val = scale.read_average(80);
Serial.println(val);
}
}
Библиотеку для работы с АЦП HX711 можно скачать здесь. Для получения данных используется библиотечная функция read_average(80), в которой производится усреднение по 80 отсчетам АЦП за время приблизительно равное 1 сек. Т.е. хост получает от ардуинки приблизительно один отсчет в секунду. Этот отсчет уже усреднен в ардуинке по 80 отсчетам, полученным непосредственно от АЦП.
Программа для хоста написана на Python и приведена ниже:
Скрытый текст
import time
import serial
import curses
import numpy as np
ser = serial.Serial('/dev/ttyUSB0', 57600) # Поправить параметры, ежели что...
coef = 0.004165622 # Калибровочный коэффициент
offs = 370162 # Смещение
cgh = 3600.0*coef # Коэффициент для пересчета скорости отбора в г/час
tim = np.zeros(60) # Массив для хранения моментов времени отсчетов
codes1 = np.zeros(60) # Массив для хранения выборок АЦП, усредненных за 1 сек
codes10 = np.zeros(60) # Массив для хранения усредненных за 10 сек значений
codes30 = np.zeros(60) # Массив для хранения усредненных за 30 сек значений
codes60 = np.zeros(60) # Массив для хранения усредненных за 1 мин значений
stdscr = curses.initscr() # Основное окно curses
curses.curs_set(0) # Убираем изображение курсора
curses.noecho() # Убираем эхо при нажатии клавиш
curses.cbreak() # Убираем необходимость Enter для ввода с клавиатуры
stdscr.keypad(True) # Возможность ввода спецсимволов (PgUp, Left etc)
stdscr.nodelay(True) # Неблокирующий режим (не ждем getch())
tst = time.time() # Начало работы
wFlag = False # Флаг протоколирования данных в файле, True - пишем файл
ser.readline() # Пропустим парочку значений - обычный мусор
ser.readline()
traw = 13 # Положение таймера при записи файла (
while True:
'''Обработка возможных событий от клавиатуры'''
ch = stdscr.getch() # Получаем код нажатой клавиши
if ch == ord('q'): # Завершение работы
break
elif ch == ord('t'): # Коррекция смещения (тары)
offs = codes60[0] # Корректируем по усредненному за последние 60 сек значению
elif ch == ord('s'): # Включить/выключить запись в файл
wFlag = not wFlag
if wFlag: # Начинаем запись лога
f = open('log.txt', 'w')
tstf = tim[0]
line = '\tВес\t\t\t\tПроизводная\t\t\tКод АЦП\n'
f.write(line)
line = 't,сек\t1 сек\t10 сек\t30 сек\t1 мин\t10 сек\t30 сек\t1 мин\t'
line += '1 сек\t10 сек\t30 сек\t1 мин\n'
f.write(line)
else: # В противном случае закроем файл
f.close()
'''Работаем с данными'''
now = time.time() - tst # Текущее относительное время
try: # Пытаемся получить данные с последовательного порта (от АЦП)
code = float(ser.readline().decode())
except: # Нечисловые данные пропустим
continue
codes1 = np.roll(codes1, 1) # "Прокручиваем" все массивы вправо на 1 позицию
codes10 = np.roll(codes10, 1)
codes30 = np.roll(codes30, 1)
codes60 = np.roll(codes60, 1)
tim = np.roll(tim, 1); tim[0] = now
codes1[0] = code # Новое значение принятого кода помещаем в начало массива
'''Массивы небольшие, поэтому рекурентные формулы для сердних не используем,
а вычисляем средние тупо по всему массиву'''
codes10[0] = np.mean(codes1[:10])
codes30[0] = np.mean(codes1[:30])
codes60[0] = np.mean(codes1)
mass1 = coef*(codes1[0] - offs) # Считаем соответствующие массы (веса)
mass10 = coef*(codes10[0] - offs)
mass30 = coef*(codes30[0] - offs)
mass60 = coef*(codes60[0] - offs)
'''Производные считаем при помощи линейной регресии (функция polyfit в
numpy) и сразу нормируем результат в g/час.'''
d10 = np.polyfit(tim[:10], codes10[:10], 1)[0]*cgh
d30 = np.polyfit(tim[:30], codes30[:30], 1)[0]*cgh
d60 = np.polyfit(tim, codes60, 1)[0]*cgh
'''Выводим информацию в файл и на консоль'''
stdscr.erase()
if wFlag:
ltim = '% .1f'%(tim[0] - tstf);
stdscr.addstr(traw, 20, ltim)
linem = '%.3f\t%.3f\t%.3f\t%.3f'%(mass1, mass10, mass30, mass60)
lined = '%.1f\t%.1f\t%.1f'%(d10, d30, d60)
linec = '%d\t%d\t%d\t%d'%(round(codes1[0]), round(codes10[0]),
round(codes30[0]), round(codes60[0]))
line = ltim + '\t' + linem + '\t' + lined + '\t' + linec
f.write(line + '\n'); f.flush()
stdscr.addstr(1, 1, '%.1f'%(tim[0]))
stdscr.addstr(3, 25, '---------------Окно----------------')
raw = 4; col = 25; dc = 12
stdscr.addstr(raw, col, '1 сек'); col += dc
stdscr.addstr(raw, col, '10 сек'); col += dc
stdscr.addstr(raw, col, '30 сек'); col += dc
stdscr.addstr(raw, col, '1 мин')
raw += 2; col = 2
stdscr.addstr(raw, col, 'Вес, г'); col += 22
stdscr.addstr(raw, col, '% .3f'%(mass1)); col += dc
stdscr.addstr(raw, col, '% .3f'%(mass10)); col += dc
stdscr.addstr(raw, col, '% .3f'%(mass30)); col += dc
stdscr.addstr(raw, col, '% .3f'%(mass60))
raw += 1; col = 2
stdscr.addstr(raw, col, 'Производная, г/час'); col += 34
stdscr.addstr(raw, col, '%.1f'%(d10)); col += dc
stdscr.addstr(raw, col, '%.1f'%(d30)); col += dc
stdscr.addstr(raw, col, '%.1f'%(d60))
raw += 1; col = 2
stdscr.addstr(raw, col, 'Код АЦП'); col += 22
stdscr.addstr(raw, col, '%d'%(round(codes1[0]))); col += dc
stdscr.addstr(raw, col, '%d'%(round(codes10[0]))); col += dc
stdscr.addstr(raw, col, '%d'%(round(codes30[0]))); col += dc
stdscr.addstr(raw, col, '%d'%(round(codes60[0])))
raw += 2; col = 2; traw = raw
stdscr.addstr(raw, col, 'Запись в файл [ ]')
if wFlag:
stdscr.addstr(raw, col + 15, 'x')
raw = 20
stdscr.addstr(raw, 1, 't - тара (коррекция смещения)'); raw += 1
stdscr.addstr(raw, 1, 's - вкл/выкл запись данных в файл'); raw += 1
stdscr.addstr(raw, 1, 'q - выход')
stdscr.refresh()
# Убираем за собой...
try: f.close()
except: pass
stdscr.keypad(False)
curses.nocbreak()
curses.echo()
curses.endwin()
Скрипт подробно прокомментирован, если кому-нибудь будет интересны детали реализации. Здесь только несколько общих комментариев.
Данные с последовательного порта периодически считываются и помещаются в кольцевой массив из 60 элементов. Затем, по этим данным, вычисляются скользящие средние веса (для окон 10, 30 и 60 сек) и их производные (это наша "скорость отбора"). Производные вычисляются при помощи линейной регрессии по соответствующему количеству отсчетов (функция plyfit() библиотеки numpy). Предусмотрена запись в файл всех данных: время, средние веса и производные веса по времени, рассчитанные в разных окнах усреднения и исходные коды, полученные от ардуинки. Запись производится в обычном текстовом формате. Таблицы потом можно непосредственно загрузить в Libre Calc или Excel для визуализации в виде графиков или дальнейшего анализа. Интерфейс с пользователем осуществляется при помощи библиотеки curses. Вид окна работающего приложения показан на следующем рисунке.
Если кто-нибудь захочет повозиться со своими весами, то скрипт должен работать безо всяких изменений на любом компьютере под Linux. На Ubuntu и Raspbian работал. Если пытаться запускать под Windows, то, возможно, потребуются какие-то танцы с curses. Но точно не знаю. Не пробовал.
3. Шумы ненагруженных весов
Рассмотрим характер сигналов с ненагруженных весов. Сначала на временном интервале 100 сек. Эти сигналы приведены на следующем рисунке.
Синяя кривая на верхней диаграмме - это, по сути, заводской режим работы - 1 отсчет в секунду безо всякой дополнительной фильтрации (усреднения). Эта диаграмма была записана еще до переключения HX711 в 80-герцовый режим. По характеру сигнала и стандартному отклонению 0.4657, мы видим, что весы вполне вписываются в декларируемую изготовителем точность 1 г. Но нам нужна точность повыше. Поэтому остальные диаграммы получены уже в режиме работы АЦП 80 Гц с применением низкочастотной фильтрацией (усреднения) с периодами (окнами) усреднения от 1 сек и выше. Эти сигналы показаны на этой же диаграмме и, в растянутом масштабе, на нижней диаграмме. Соответствующие стандартные отклонения приведены в легенде справа. Из этих данных легко видеть, что на коротких временах, уровень шума 10-кг весов вполне реально сделать существенно меньше 0.1 г.
К сожалению, на более длительных интервалах (существенно больших, чем окно усреднения) низкочастотный шум (дрейф) "возвращает" заявленную производителем точность весов 1 г. Это хорошо видно на следующем рисунке, где представлена длительная (более 40 часов) запись сигнала с ненагруженных весов, оставленных на выходные в изолированном, закрытом помещении.
Таким образом, при длительных измерениях веса 10 килограммовыми весами, необходимо ориентироваться на погрешность не менее, чем заявлена производителем. Т.е. - 1 грамм.
Тем не менее, данные весы предназначены для измерения скорости отбора. Т.е. производной весы по времени. Поэтому интересно было посмотреть шум именно производной, а не самого веса. На следующем рисунке на верхней диаграмме представлена запись производной сигнала ненагруженных весов в течение 10 мин. Для сравнения, на нижней диаграмме приведена соответствующая запись самого веса.
На верхней легенде справа приведены стандартные отклонения производной веса по времени. Эти цифры дают возможность оценить точность, которую можно достичь при различных окнах усреднения. Видно, что при использовании окна 10 сек, мы можем рассчитывать на погрешность измерения скорости отбора никак не менее, чем десяток-другой грамм/час. При отборе голов эта погрешность сопоставима с самой величиной. Для окон 30 сек и 1 мин ситуация лучше - несколько грамм/час и порядка одного грамма/час соответственно. Тем не менее, необходимо учесть, что информацию о нештатном поведении узла отбора мы получим с опозданием на одну-две минуты. Т.е. такие датчики можно использовать лишь для некритических аварийных ситуаций. Ну, например, засорение жиклера клапана отбора...
В следующем топике я расскажу о чувствительности моих 10-килограммовых весов при статических и линейно нарастающих нагрузках.