Часть 3. Доплеровский расходомер, антикризисный вариант

Цифровая часть расходомера выглядит примерно так:

Ааааа... мутная схема

Не переживайте, файл от P-cad-2002 в формате ASCII, а также тривиальный jpeg, прилагается (Схема расходомера). Эта картинка - так, чтобы что-то было перед глазами.

Итак, по-тихому применим ту же самую схему интерфейса 1-Wire, что и в предыдущей статье. Изменений немного: установим формирователь сигнала «сброс» наподобие DS1811 (их очень много выпускается - Analog Devices, Dallas Semiconductor, AMD, Linear Tecnology... годится любой, с активным уровнем «0» и на контролируемое напряжение 5 вольт), да частоту кварца поднимем до 16 МГц. Разъем программатора заменен на разъем JTAG внутрисхемного эмулятора. Для постоянной генерации ультразвука используем выход OCR1B таймера 1, АЦП используем встроенный, канал 0 - для оцифровки выделенного доплеровского сигнала, а канал 1 - для измерения напряжения АРУ.

Для раскачки ПЭА, полное входное сопротивление которого на резонансной частоте - около 40 Ом, используем микросхему D3, MAX628 или MAX1428, в состав которой входят два двухтактных буфера, один инвертирующий, другой - неинвертирующий, с выходными каскадами на комплементарной паре полевых транзисторов. Допустимый выходной ток - 2А. Напряжение поднимем до 8-9 вольт трансформатором  T1. Вот собственно и всё, никаких чудес.

Теперь рассмотрим аналоговую обработку сигнала. Принятый с приемного ПЭА сигнал по амплитуде ВЧ несущей составляет несколько милливольт. В принципе, для приема доплеровского сигнала, его переноса с несущей частоты на нулевую и последующего усиления, вполне годится схемотехника приемников прямого преобразования, вернее их разновидность - DSB. Только нам проще - не нужно строить цепей ФАПЧ для гетеродина, у нас под руками есть несущая частота в ее первозданном виде. В целом получается нечто такое:

Трансформатор Т2 обеспечивает развязку с ПЭА и согласование входного каскада с ним. Сигнал сразу же отправляем на УВХ на транзисторе VT1 и конденсаторе C13 - на нем сделан фазовый детектор (чувствительный и к амплитуде сигнала). Далее усиливаем доплеровский сигнал, уже перенесенный на нулевую частоту, обычной транзисторной «двойкой» с коэффициентом усиления около 30, заодно подрезаем на нём частоты выше 10 кГц. Далее сигнал отправляется на усилитель-формирователь АЧХ на ОУ D4. Он имеет АЧХ, приведенную на рисунке.

Как видим, усилитель имеет плавный подъем АЧХ, максимум на частоте около 4 кГц. Это обусловлено тем, что (не буду вдаваться в теорию) амплитуда доплеровского сигнала растет с частотой. Подъем АЧХ около 3 дб/декаду частично компенсирует это явление. Кроме того, при установке ПЭА не на противоположных стенках трубы, а вблизи друг друга, имеет место неприятное явление: на некоторых режимах течения вблизи стенки трубы появляются мощные завихрения, которые порождают низкочастотную составляющую сигнала, по амплитуде (из-за того, что источник находится вблизи ПЭА) порой превосходящую полезный сигнал. Приведенная на рисунке форма АЧХ позволяет в определенной мере снизить влияние этой помехи. Спад на частотах выше 4 кГц необходим во избежание явления передискретизации на АЦП. Конечно, этими фильтрами можно еще поиграться, ибо нет предела совершенству.

Для упрощения обработки сигнала усилитель охвачен петлей АРУ. На полевом транзисторе VT4 и резисторе R12 выполнен управляемый делитель напряжения. Управляется он детектором на транзисторе VT5. Необходимую задержку вносит конденсатор С21. Номиналы подобраны так, что амплитуда сигнала на выходе около 3 вольт (от пика до пика). Подбирая резистор R16, можно уточнить этот параметр. Особо продвинутые пользователи могут заменить этот узел на потенциометр с цифровым управлением, например на AD8400, и управлять им программно. Просто мне не хотелось загромождать программу лишними модулями. В качестве детектора (программного) можно конечно просто искать максимум в массиве, но я предпочитаю считать среднеквадратичное отклонение. Сигнал у нас шумоподобный, а для него амплитудные значения обычно не превышают двух СКО. Далее разницу от необходимого уровня отправляем на интегратор (программный же), и получаем программно-управляемую петлю АРУ. Как-нибудь впоследствии можем разобрать подробнее, если кому будет интересно. А пока что - уровень напряжения на выходе детектора АРУ мы будем использовать для определения факта наличия доплеровского сигнала (ессно, чем больше сигнал, тем больше требуется ослабление и тем больше будет напряжение АРУ; исходя из параметров применяемого полевого транзистора, отсечка начинается с напряжения АРУ около 3.5 вольт).

Трансформаторы Т1 и Т2 - одинаковые, выполнены на броневых сердечниках KB5*2 700НМ-21-160. Обмотка, подключенная к ПЭА - 16 витков ПЭВ D=0.35, индуктивность ее - 41 мкГн ±5%. Обмотка, подключенная к схеме - 10 витков ПЭВ D=0.35, ее индуктивность - 18 мкГн ±5%.

У сердечников с зазором есть неприятное свойство: микрофонный эффект. Может быть, в качестве входного трансформатора лучше использовать сердечник без зазора, проклеенный, из материала 100ВЧ или 50ВЧ. Не пробовал за неимением.

 

Теперь перейдем к программному обеспечению.

Прежде всего - о методе определения средней частоты доплеровского сигнала. Те, кто видел схемы доплеровских расходомеров скажут: а на хрена тут спектральный анализ, путевые люди обходятся обыкновенным частотомером. Напомню, что ширина полосы белого шума пропорциональна количеству экстремумов за единицу времени, а вовсе не количеству переходов через ноль, как это сделано в подавляющем большинстве таких расходомеров. А чтобы считать экстремумы, нам нужно продифференцировать шум, а это фильтр первого порядка, крутизна его - 20 дб/декаду. Если мы захотим иметь расходомер с коэффициентом перекрытия по скорости потока 100, то потребность в динамическом диапазоне 40 дБ мы закладываем уже в дифференциаторе. Создали себе трудности, теперь помучаемся, типа. Уж лучше озаботиться спектральным анализом и в разы упростить аналоговую часть (в особенности, ее настройку и повторяемость).

 

Для начала покажу, что нам, собственно, надо обработать. Приведу пример двух спектров, полученных именно на этом железе (на AtMega16, длина преобразования- 256 слов, соответственно энергетический спектр - 128 отсчетов). Спектры сигналов Доплера получены при одинаковом расходе достаточно тухлой воды, на трубопроводе 50 мм, но различными типами ПЭА.

 

- картинка получена с использованием ПЭА от расходомера «Взлет», установленными диаметрально противоположно.

 

- ПЭА фирмы Greyline (SE4), приемный и передающий ПЭА совмещены в одном корпусе.

 

Можно сразу же отметить, что спектр с совмещенного ПЭА имеет значительно большее содержание низкочастотных составляющих, он ближе к «белому». Причина этого в том, что рассеянный сигнал принимается по всему сечению трубопровода. При оппозитной установке основной сигнал имеет явный максимум, поскольку диаграмма направленности ПЭА такова, что сигнал Доплера принимается в основном из центральной части потока. Для обработки компаратором он, конечно, больше подходит, но всё равно наберемся погрешностей, в том числе из-за неучёта профиля потока. Значительно более точная оценка - определение центра тяжести распределения. Именно этот метод и реализован в программе, кою при сём препровождаю.

Программа написана на языке Си. Компилятор - IAR C/EC++ Compiler for AVR 3.10C. Программа сделана для двух вариантов аппаратуры - AtMega32, запущенной с кварцем 16 МГц, или AtMega16 или 8, и на частоте 8 МГц - более доступный вариант. Для переключения на AtMega16(8) в опциях проекта, в категории ICCAVR на закладке Preprocessor нужно указать Defined symbols - M16. Это необходимо для условной компиляции. (Полный текст программы расходомера)

В общем виде алгоритм работы расходомера следующий.

1. Собираем в буфер длиной 512(256 для Mega16) слов отсчеты АЦП с темпом  9615 отсчетов в секунду. Сбором занимается обработчик прерывания АЦП

#pragma vector = ADC_vect

__interrupt void _ADC_end_conversion(void)

{

Сначала – не мешаем работать каналу 1-Wire. Для этого быстренько считываем значение АЦП, сбрасываем его флажок, и разрешаем глобальные прерывания. Тогда прерывания от канала 1-Wire смогут работать как вложенные.

  …………………………

  temp=ADCH;

  ADCSRA_Bit4 = 1;

  __enable_interrupt();

  …………………………

Чтоб начать укладывать в буфер данные, надо в meter._adc_status положить значение _ADC_RUN, и дальше всё пойдет само собой

  switch(meter._adc_status)

  {

    case _ADC_RUN:

      …………………………………………

 

    case _ADC_RUNNING:

      // получим измерение канала (8 бит) 0 и положим в буфер

      meter.fx[meter._adc_samples_ptr++] = (signed int)((signed char)(temp)ˆ0x80); // оцифровываем 8 бит и создаем знак полярности

Когда прерывание добежит до конца буфера, выставит состояние _ADC_READY, и начнет измерять напряжение АРУ, и так – до следующего запуска.    

    if(meter._adc_samples_ptr >= M_LEN)

      {

        meter._adc_status=_ADC_READY; // конец накопления буфера

        meter._adc_samples_ptr = 0;

      }

      if(meter._adc_samples_ptr == M_LEN-1) // последнее измерение канала 0

        ADMUX = 0x61; // channel 1, Vref=5v Vcc, result left aligned

      break;

    default:

      // запомним измерение канала 1 (8 бит, не более)

      meter.gain_control = temp;

    break;

  }

}

2. Переходим в частотную область. Для этого производим над собранным буфером преобразование Хартли.

    RFHT(meter.fx); // Быстрое преобразование Хартли

3. Получаем энергетический спектр из результатов преобразования.

    amp_accumulator(meter.fx, meter.fq, x_accum);

Поскольку не все частоты присутствуют в текущем окне наблюдения, неизбежно появление дыр в энергетическом спектре. Поэтому накапливаем его в отдельном буфере длиной 256(128) байт. Для накопления используем  256(128) фильтров ФНЧ, а константы для этого фильтра считаем на количество отсчетов, на котором значение выхода фильтра получается 38% от установившегося значения. (int compute_x_accum(unsigned char d))

Проверяем, сколько прошло времени от предыдущего обсчета измерений.

    tick_pass = _difftime(tick = _get_tick_count(), tick_loop);

    if(tick_pass >= _TIME_2_TICK(0.2))

    {

      tick_loop = tick;

Все дальнейшие операции мы делаем неторопливо, только спектры накапливаем с максимально возможным темпом. Итак, если 200 мс от предыдущего замера не прошло – возвращаемся к п.1.

4. Среднюю частоту в энергетическом спектре Доплера (после накопления) определяем как центр тяжести распределения (float weigth_center(int * fq)). Этот метод позволяет получить отсчет центра тяжести с дискретом в разы меньше, чем имеющийся шаг по частоте в массиве. (статистика, блин…)

      f_center = weigth_center(meter.fq) * FREQ_STEP;

5. По величине напряжения АРУ оцениваем амплитуду доплеровского сигнала.

Если амплитуда нас удовлетворяет, то пересчитываем центр тяжести спектра в соответствующую ему частоту и отправляем результат на фильтр 4-го  порядка (iir_4ord(f_center, &fctrl)); если же амплитуда мала – отправляем на вход фильтра «0».

      if(meter.gain_control > setup.gain_limit)

      { // есть расход

        is_dopler |= 3;

        meter.dopler_freq = iir_4ord(f_center, &fctrl);

      }

      else

      { // нет расхода

        is_dopler &= ˜1;

        meter.dopler_freq = iir_4ord(0, &fctrl);

      }

6. По формуле [4] пересчитываем среднюю частоту в скорость потока,

      // скорость потока получаем в метрах в секунду

      meter.flow_speed = (meter.dopler_freq * setup.sensor_phase_speed) /

                         (2.0 * BASE_US_FREQ);

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

      // расход получаем в литрах в секунду

      meter.flow_rate = meter.flow_speed * Pi/4 * SQR(setup.pipe_inner_diameter) * 1E-3;

Если на шаге 5 мы определили наличие расхода, умножаем текущий расход на время между измерениями и прибавляем к суммарному счетчику-накопителю (получается прокачанный объем).

      if(is_dopler & 0x01)

      { // есть сигнал, есть расход

        // просуммируем объемный расход за время цикла

        meter.totaliser += volume_to_totaliser(meter.flow_rate, tick_pass);

        if((++_save_prescaler) >= TOTALISER_SAVE_PERIOD)

        { // Один раз в минуту сохраняем суммарный счетчик

          _save_prescaler = 0;

          _save_totaliser(meter.totaliser);

        }

      }

      else

      { // нет расхода

        if(is_dopler&0x02)

        {

          // сигнал только что исчез

          is_dopler &= ˜0x02;

          // добавим последнее значение в накопитель

          meter.totaliser += volume_to_totaliser(meter.flow_rate, tick_pass);

          // запишем последнее значение накопителя

          _save_totaliser(meter.totaliser);

        }

        _save_prescaler = 0;

      }

7. Счетчик – накопитель сохраняем в энергонезависимой памяти.

Здесь остановлюсь чуть подробнее. Дело в том, что для этого обычно применяется flash. А у нее количество циклов записи хоть и велико, но конечно; если писать в одно и то же место скажем раз в секунду, то за год можно прописать это место насмерть (на самом деле при этом начинает глючить всё на кристалле). Поэтому запись мы будем производить раз в минуту, записывать будем по очереди в 50 ячеек, а по включению, для того, чтобы восстановить значение счетчика, будем в этой области искать единственную запись с правильной CRC. Такой метод работает годами и переживёт меня (см. процедуры void _save_totaliser(float value) и float _load_totaliser(void)).

8.Напоследок смотрим, не надо ли часом выполнить какую-нибудь из «отложенных» команд, пришедших по каналу 1-Wire.

      if(_1w.ext_event != 0)

      {

        // обработка внешнего события - запись в EERPOM, перенастройка setup

        switch(_1w.ext_event)

        {

          case 0x4e: // set value to totaliser

            meter.totaliser = *((float *)(&_1w.rw_buffer[1]));

            _save_totaliser(meter.totaliser);

            break;

          case 0x99: // write and apply new @setup@

            memcpy(&setup, &_1w.rw_buffer[2], sizeof(T_setup));

            save_setup();

            iir_4ord_init(setup.smooth_time, &fctrl);

            x_accum = compute_x_accum(setup.average_times);

            break;

        }

        _1w.ext_event = 0;

      }

Ну а теперь вкратце рассмотрим функции.

Файл Lowlevelinit.c

void _startup_init(void)

Это начальная инициализация аппаратуры. Без комментариев – Atmel не один даташит выпустил, чтоб я их еще и переписывал – не дождетесь.

Частота выходного сигнала для генерации ультразвука описана в jeneric.h

#define BASE_US_FREQ 640000.0.

Частота указывается в Герцах и округляется при пересчете.

Файл 1-w_hdr.c

Этот файл нам уже знаком по «Как сделать самому ведомое устройство, понимающее протокол 1-Wire™, из подручных материалов.» Изменения его не коснулись, только пересчитаны константы задержек на 16 МГц, да добавлен ряд специфичных команд обращения к памяти/управления. Сейчас мы их разберем.

0xBE: read from scratchpad.

Этой командой мы читаем структуру готовых данных расходомера. Структура следующая:

typedef struct

{

  float flow_rate;   //4 байта – расход в л/с

  float totaliser;   //4 байта – суммарный расход в мˆ3

  unsigned char gain;//1 байт – напряжение АРУ в кодах АЦП

  float flow_speed;  //4 байта – скорость потока в м/с

}T_scratch;

Тип данных float располагается в памяти точно так же, как и в компъютере – Intel’овский формат. (1.0 в памяти лежит как 00h 00h 80h 3fh). Он соответствует типу Single в Delphi.  Тип Int  лежит также в интеловском порядке – младший, старший байты.

Формат пакета следующий:

[Длина  пакета = 13 байт]{13 байт T_scratch}[CRC]

Если нам не нужны все данные, мы можем прервать чтение в любой момент, но останемся без CRC. В CRC входят все указанные байты.

0x4E:  write to scratchpad

В этой команде передаем Intel Floating Point, т.е. 4 байта, и CRC. Это значение записывается в суммарный счетчик – накопитель.

[0x4E]{Intel FP}[CRC].

Если расходомер примет пакет и CRC будет некорректной, команда не будет обработана. В CRC входят байт команды 0x4E и все данные.

0x66:  read memory

Эта команда позволяет читать область настроек расходомера. Формат пакета аналогичен 0xBE, а структура имеет следующий вид:

typedef struct

{  

  float sensor_phase_speed;  // 4 фазовая скорость датчика в м/с, 4200

  float pipe_inner_diameter; // 4 внутренний диаметр в мм

  unsigned char gain_limit;  // 1 минимальное напряжение АРУ в кодах АЦП

  unsigned char average_times; // 1 количество накоплений спектра в замерах

  unsigned char smooth_time;   // 1 время сглаживания расхода в секундах

}T_setup;

 

0x99:  Write memory

Запись этой же структуры. Формат пакета

[0x99]{T_setup}[CRC]

Подтверждения никакого нет, поэтому желательно проверять результат операции чтением. CRC считается так же, как и в команде 0x4E

0xBB: Read Memory Random

Формат команды:

[BB][Lo-address][Hi-Address][Len]

Ответ

{поток данных длиной Len}[CRC]

Эта команда просто читает память.  Нулевой адрес соответствует началу структуры meter. Иногда бывает крайне любопытно посмотреть на сигнал, на спектр, на промежуточные результаты вычислений… в общем, команда для отладки. CRC считается так же, как и в команде 0xBE

Файл DSP_Hartley.c

Здесь собраны функции цифровой обработки сигналов.

void RFHT(int * fx) - быстрое преобразование Хартли;

Параметр - массив длиной 512(256) слов. Результат преобразования возвращается в нём же.

void amp_accumulator(int * fx, int * fq, int x) - накопление амплитуд гармоник в массиве;

Параметры: int * fx - разложение в спектр после RFHT; int * fq - указатель на массив, в котором накапливается энергетический спектр; int x - коэффициент накопления.

int compute_x_accum(unsigned char d) - расчет коэффициента x для amp_accumulator;

параметр d - это количество накоплений, за которое выходной сигнал устанавливается 36.8% от установившегося значения.

float weigth_center(int * fq) - вычисление центра тяжести массива fq как распределения.

Файл EE_block.c

void eeprom_write(void __eeprom * dest, void * src, int n)

void eeprom_read(void * dest, void __eeprom * src, int n)

Две функции для работы с EEPROM как массивом. Понадобился по причине неумения memcpy, memset и прочие работать с областью EEPROM, хотя обычные функции работы с  переменными - работают. Недоделка компилятора!

Файл IIR_4_ord_filter.c

Название говорит само за себя.

void iir_4ord_init(float F0, t_fctrl * fctrl) - расчет констант фильтра. В качестве параметра F0 указываем число секунд, за которые выходное значение фильтра достигает 36.8% от установившегося.

float iir_4ord(float v, t_fctrl * fctrl) - собственно сама фильтрация.

Структура t_fctrl описана в IIR_4ord_filter.h.

Файл Timer.c

Здесь лежит обработчик прерывания системного таймера, которое вызывается каждые 16.384 мс.

unsigned long _get_tick_count(void) - получение текущего значения времени в 64(128)-мкс дискретах;

unsigned long _difftime(unsigned long t, unsigned long start_t) - вычисление разности двух времен.

Перевод времени из тиков таймера в секунды и обратно описан в Timer.h. В нем есть две функции перевода - _TIME_2_TICK(a) и _TICK_2_TIME(a).

Файл Main.c

Кроме тривиальной void main(void) в этом файле есть еще несколько функций:

float volume_to_totaliser(float flow_rate, unsigned long time) - эта функция рассчитывает суммрный объем из расхода и времени. Результат просто добавляем к суммарному счетчику. Параметр time указывается такой, как возвращается из функций timer.

void _save_totaliser(float value) - сохраняет суммарный счетчик во флэш - памяти, с функцией распределенной записи;

float _load_totaliser(void) - восстанавливает последнее записанное функцией save_totaliset значение из области EEPROM

void save_setup(void), void load_setup(void)  - работа с настройками устройства. Сохранённые настройки защищены CRC, при несовпадении с ним при чтении - восстанавливаются значения по умолчанию.

Ну вот в общих чертах и ... далеко не всё, что еще знаю. Исходные тексты программы, принципиальная схема и печатная плата в формате ASCII P-cad 2002 - прилагаются.

В качестве бонуса прилагаю и оболочку, написанную на Delphi. Она работает с данным расходомером через преобразователь RS232 - 1Wire, на специализированной микросхеме - DS2480. Рассмотрение принципов ее работы в рамки данной статьи как-то не вписывается, да оно тривиально и хорошо разжевано в фирменном даташите. Схему на всякий случай - прилагаю (у меня сделан вариант с полной гальванической развязкой, от греха)

 

 

Вся эта схемка разместилась в корпусе разъема DB-9, питается она от питания устройства, 7-15 вольт. D5 - 78L05, D4 - NKE0505D (DC-DC конвертер с гальванической развязкой).

С этим интерфейсом оболочка позволяет получить и вывести на дисплей текущие параметры расходомера, получить спектр доплеровского сигнала, записать значения в setup.

 

Основная форма. Справа - 128 отсчетов спектра, слева - значения: расход в л/с, суммарный объем, амплитуда доплеровского сигнала в кодах АЦП, скорость потока в м/с. Еще ниже - частота Доплера в Герцах. Ее можно использовать для тарировки расходомера.

 

 

Форма настройки расходомера. Параметры - фазовая скорость датчика в м/с, диаметр трубопровода (внутренний) в мм, порог по амплитуде сигнала Доплера - в комментариях, я надеюсь, не нуждаются. Далее - количество накоплений энергетического спектра (128 - это перебор, признаюсь. 64 - вполне достаточно). И последний параметр - постоянная времени сглаживающего фильтра на измеренную частоту Доплера. Параметр целочисленный, это количество секунд, необходимое выходу фильтра для достижения уровня 36.8% от установившегося.

Несколько слов о тарировке расходомера. Выше я уже писал, что основным параметром для накладного ПЭА является его фазовая скорость, то есть отношение скорости звука в материале призмы к синусу угла установки ПЭА от вертикали. На самом деле - это первое приближение, оно не учитывает плотности распределения  энергии сигнала по спектру, особенно - для совмещенного ПЭА. Поэтому для обеспечения точности измерений необходимо скорректировать значение, полученное расчетным путем, по образцовому расходомеру.

          (1)

Эту формулу получаем из выражения (4) части первой. Частоту Доплера Δω  - получаем от нашего, тарируемого, расходомера; скорость потока V - контролируем образцовым. Получившееся значение фазовой скорости есть незыблемая характеристика датчика; ее следует высечь на камне и беречь для потомков. К слову сказать, фирма «взлет» пишет в паспорте своих ПЭА эту цифру; ею можно пользоваться для этого расходомера.

Обсудить;      Далее --->