Посмотр рубрик

Измерение параметров сигналов

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

Цифровые #

ШИМ #

Простой "квадратный" сигнал и его производные часто используются для передачи данных и как выход с "датчика":

  • Сигнал управления модельными серво и драйверами моторов (ширина импульса в мкс при частоте 50 Гц) - этот сигнал можно "перехватить" Ардуиной, например с приёмника или полётного контроллера дрона
  • Значение в виде заполнения ШИМ, например некоторые датчики CO2
  • Сигнал любого тахо-датчика, например 4-пин вентилятор или мотор с датчиком холла

Все эти задачи можно решить при помощи одного и того же подхода:

  • В обработчике внешнего прерывания по изменению измерять время, прошедшее с прошлого прерывания
  • Текущее состояние просто инвертировать, либо измерять по факту (чтение пина может быть относительно долгим)
  • Запоминать время высокого и низкого состояний на основе текущего состояния - из них можно получить все характеристики квадратного сигнала

Рассмотрим готовый класс:

class PWMMeter {
   public:
    // инициализировать текущим состоянием пина
    void init(bool state) {
        noInterrupts();
        _state = state;
        _tPrev = 0;
        _ready = false;
        interrupts();
    }

    // вызывать в прерывании по CHANGE
    void pinChange() {
        pinChange(!_state);
    }

    // вызывать в прерывании по CHANGE с указанием сигнала пина
    void pinChange(bool state) {
        if (_state == state) return;

        _state = state;
        if (!_tPrev) {
            _tPrev = micros();
        } else {
            uint32_t us = micros() - _tPrev;
            _tPrev += us;
            if (!_tPrev) ++_tPrev;
            if (_state) {
                _tLow = (_tLow + us) >> 1;
                _tHigh = (_tHigh + _tHighBuf) >> 1;
                _ready = true;
            } else {
                _tHighBuf = us;
            }
        }
    }

    // период измерен, само сбросится в false
    bool ready() {
        return _ready ? _ready = false, true : false;
    }

    operator bool() {
        return ready();
    }

    // получить длину высокого импульса в мкс
    uint32_t getHigh() {
        noInterrupts();
        uint32_t high = _tHigh;
        interrupts();
        return high;
    }

    // получить длину низкого импульса в мкс
    uint32_t getLow() {
        noInterrupts();
        uint32_t low = _tLow;
        interrupts();
        return low;
    }

    // получить период в мкс
    uint32_t getPeriod() {
        noInterrupts();
        uint32_t prd = _tHigh + _tLow;
        interrupts();
        return prd;
    }

    // получить частоту в Гц
    uint32_t getFreq() {
        uint32_t prd = getPeriod();
        return prd ? 1000000ul / prd : 0;
    }

    // получить частоту в Гц
    float getFreqFloat() {
        uint32_t prd = getPeriod();
        return prd ? 1000000.0f / prd : 0;
    }

    // получить частоту в об/мин
    uint32_t getRPM() {
        return getFreq() * 60ul;
    }

    // получить заполнение ШИМ в процентах
    uint8_t getDutyPercent() {
        noInterrupts();
        uint32_t prd = _tHigh + _tLow;
        uint32_t high = _tHigh;
        interrupts();
        return prd ? (high * 100ul) / prd : 0;
    }

    // получить заполнение ШИМ в 8 бит
    uint8_t getDuty8() {
        noInterrupts();
        uint32_t prd = _tHigh + _tLow;
        uint32_t high = _tHigh;
        interrupts();
        return prd ? (high << 8) / prd : 0;
    }

    // получить заполнение ШИМ в 16 бит
    uint16_t getDuty16() {
        noInterrupts();
        uint32_t prd = _tHigh + _tLow;
        uint32_t high = _tHigh;
        interrupts();
        return prd ? (high << 16) / prd : 0;
    }

    // получить заполнение ШИМ в диапазоне 0-1
    float getDutyFloat() {
        noInterrupts();
        uint32_t prd = _tHigh + _tLow;
        uint32_t high = _tHigh;
        interrupts();
        return prd ? float(high) / prd : 0;
    }

    // прошло с последнего фронта, мкс
    uint32_t lastChange() {
        noInterrupts();
        uint32_t us = _tPrev;
        interrupts();
        return micros() - us;
    }

   private:
    volatile uint32_t _tHighBuf = 0;
    volatile uint32_t _tHigh = 0, _tLow = 0;
    volatile uint32_t _tPrev = 0;
    volatile bool _state = false;
    volatile bool _ready = false;
};

Реализовано:

  • Асинхронное измерение времени высокого и низкого состояний
  • Буферизация и защёлкивание по восходящему фронту, флаг готовности
  • Среднее арифметическое по двум измерениям
  • Получение длин импульсов, периода и частоты сигнала, а также заполнения ШИМ в разных единицах
  • Диапазон частот от 0 Гц до ~10 кГц (AVR), на более мощных платформах будет выше

Для использования нужно:

  • В начале работы передать в init() текущее состояние пина
  • Вызывать метод pinChange() в прерывании по CHANGE. При пропуске прерывания может случиться рассинхронизация фронтов (код просто инвертирует прошлое состояние)
    • Или вызывать метод pinChange(состояние) с передачей текущего состояния пина (digitalRead() или быстрый аналог, например GyverIO) - медленнее, но надёжнее
  • Опрашивать метод ready() или сам объект в условии, по условию или реже забирать результат

Пример селф-теста, пины 2 и 3 соединены проводом, 3 выдаёт сигнал, 2 измеряет:

#define SENS_PIN 2  // сюда подаётся ШИМ
#define PWM_PIN 3   // отсюда
PWMMeter pwm;

void setup() {
    Serial.begin(115200);

    // инициализация начальным значением
    pwm.init(digitalRead(SENS_PIN));

    // вызов метода pinChange в прерывании по CHANGE
    attachInterrupt(digitalPinToInterrupt(SENS_PIN), []() { pwm.pinChange(); }, CHANGE);

    // запускаем ШИМ
    analogWrite(PWM_PIN, 100);
}

void loop() {
    // по готовности, а лучше реже, с нужной для вывода частотой
    if (pwm) {
        Serial.println(String() + pwm.getFreq() + ',' + pwm.getDuty8());
        // вывод 490 Гц, заполнение 100
    }
    delay(100);
}

Чтение PWM серво (длина высокого импульса):

#define SENS_PIN 2  // пин измерения
#define SRV_PIN 3   // пин серво

#include <Servo.h>
Servo srv;
PWMMeter pwm;

void setup() {
    Serial.begin(115200);

    pwm.init(digitalRead(SENS_PIN));
    attachInterrupt(digitalPinToInterrupt(SENS_PIN), []() { pwm.pinChange(); }, CHANGE);

    srv.attach(SRV_PIN);
    srv.writeMicroseconds(1000);
}

void loop() {
    if (pwm) {
        Serial.println(pwm.getHigh());  // будет выводить ~1000
    }
    delay(100);
}

Частота #

Для измерения только частоты сигнала можно сделать алгоритм чуть проще, но добавить усреднение периодов и возможность точно измерять гораздо более высокие частоты - для этого нужно считать время не между соседними прерываниями, а между несколькими, причём это количество можно сделать автоматически настраиваемым в зависимости от частоты. Прерывание можно оставить только по фронту или спаду - это снизит нагрузку на процессор. Готовый класс измерителя частоты:

class FreqMeter {
   public:
    static constexpr uint32_t minPrd = 5000;
    static constexpr uint32_t maxPrd = 20000;
    static constexpr uint8_t minReads2 = 0;
    static constexpr uint8_t maxReads2 = 7;

    // вызывать в прерывании по FALLING/RISING
    void pinFront() {
        if (--_count) return;

        if (_tPrev) {
            uint32_t us = micros() - _tPrev;
            _tPrev += us;
            _prd = (_prd + (us >> _reads2)) >> 1;
            _ready = true;

            if (us < minPrd && _reads2 < maxReads2) ++_reads2;
            else if (us > maxPrd && _reads2 > minReads2) --_reads2;
        } else {
            _tPrev = micros();
        }
        _count = (1 << _reads2);
    }

    // сброс измерения
    void reset() {
        noInterrupts();
        _count = (1 << _reads2);
        _tPrev = micros();
        _ready = false;
        interrupts();
    }

    // период измерен, само сбросится в false
    bool ready() {
        return _ready ? _ready = false, true : false;
    }

    operator bool() {
        return ready();
    }

    // получить период в мкс
    uint32_t getPeriod() {
        noInterrupts();
        uint32_t prd = _prd;
        interrupts();
        return prd;
    }

    // получить частоту в Гц
    uint32_t getFreq() {
        uint32_t prd = getPeriod();
        return prd ? 1000000ul / prd : 0;
    }

    // получить частоту в Гц
    float getFreqFloat() {
        uint32_t prd = getPeriod();
        return prd ? 1000000.0f / prd : 0;
    }

    // получить частоту в об/мин
    uint32_t getRPM() {
        return getFreq() * 60ul;
    }

    // прошло с последнего фронта, мкс
    uint32_t lastChange() {
        noInterrupts();
        uint32_t us = _tPrev;
        interrupts();
        return micros() - us;
    }

   private:
    volatile uint32_t _tPrev = 0;
    volatile uint32_t _prd = 0;
    volatile uint8_t _count = 1;
    volatile uint8_t _reads2 = 0;
    volatile bool _ready = false;
};

Реализовано:

  • Измерение частоты и периода с автоматическим усреднением по нескольким периодам
  • Автоподстройка количества периодов - позволяет достигать хорошей точности и скорости во всём диапазоне частот
  • Среднее арифметическое по двум измерениям
  • Быстрые бинарные вычисления в прерывании
  • Диапазон частот от 0 Гц до ~140 кГц (AVR), на более мощных платформах будет выше

Примечание: при резком падении частоты импульсов алгоритм может на какое то время "зависнуть" (не поднимать флаг готовности), уменьшая количество периодов для усреднения под текущую частоту.

Использование:

  • Вызывать pinFront() в прерывании по фронту или спаду
  • Опрашивать метод ready() или сам объект в условии, по условию или реже забирать результат
#define SENS_PIN 2  // сюда подаётся ШИМ
#define PWM_PIN 3   // отсюда
FreqMeter freq;

void setup() {
    Serial.begin(115200);

    // вызов метода pinChange в прерывании по фронту/спаду
    attachInterrupt(digitalPinToInterrupt(SENS_PIN), []() { freq.pinFront(); }, FALLING);

    // запускаем ШИМ
    analogWrite(PWM_PIN, 100);
}

void loop() {
    // по готовности, а лучше реже, с нужной для вывода частотой
    if (freq) {
        Serial.println(String() + freq.getFreq());
        // вывод 490 Гц
    }
    delay(100);
}

Полезные страницы #

Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх