На практике часто бывает нужно прочитать сигнал, не относящийся к стандартным интерфейсам. Это можно делать разными способами, используя аппаратные блоки и возможности отдельных МК, но рассмотрим здесь программную реализацию на Ардуино-фреймворке, т.е. универсальные алгоритмы для всех плат и МК.
Цифровые #
ШИМ #
Простой "квадратный" сигнал и его производные часто используются для передачи данных и как выход с "датчика":
- Сигнал управления модельными серво и драйверами моторов (ширина импульса в мкс при частоте 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);
}
Полезные страницы #
- Набор GyverKIT – наш большой стартовый набор Arduino, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])
- Поддержать автора за работу над уроками