6-канальный осциллограф на Arduino
О проекте
Способ работы: (0) установить подстраиваемые переменные, (1) запустить скетч, (2) активировать плоттер последовательного порта.
Описание: Данный скетч имитирует 6-лучевой осциллограф. Ожидается, что входные сигналы, представленные на аналоговых выводах, будут находиться в диапазоне от 0 до 5 вольт с максимальной частотой 1 кГц. Подстраиваемые переменные объявляются в отдельном разделе. Эти переменные можно найти после операторов define
. Можно выбрать количество входов 1-6, которые отображают напряжения на выводах A0-A5 соответственно. Незадействованные открытые аналоговые выводы выдают ложные напряжения.
Осциллограф работает в двух режимах: «непрерывный» (свободная работа) и «принудительный запуск» («ждущий», запуск развертки при выполнении критерия). Критерий (триггер) срабатывания выполняется, когда входной сигнал, считанный с A0, пересекает предопределенное напряжение запуска, в зависимости от того, будет ли это «фронт» или «спад», когда он пересекает это предопределенное напряжение. В «ждущем» режиме общее время развертки может быть установлено в миллисекундах. Начало запуска развертки указывается, когда синхронизирующая отметка доскакивает до уровня 5 В.
При развертке выборка на аналоговых выводах будет производиться каждые 'SampleInterval
' миллисекунд. В нижней части графика синхронизирующие отметки (прямоугольный сигнал) будут переключаться каждый 10-ые 'SampleInterval
' миллисекунд.
Встроенный светодиод (вывод 13) является индикатором состояния осциллографа: (1) включен, непрерывный режим или развертка в «ждущем» режиме, (2) мигает, запущен «ждущий» режим, ожидает срабатывания триггера, (3) выключен, все операции приостановленны (с помощью кнопки).
Когда происходит выборка более одного сигнала, отображения сигналов могут «накладываться» или «разделяться по каналам». Когда используются каналы, напряжения на вертикальной оси не откалиброваны.
Опционально между цифровым выводом 12 и землей может быть подключена кнопка. При ее нажатии цифровая выборка и развертка останавливаются. Повторное нажатие кнопки возобновляет развертку (но с разрывом в графиках сигналов).
Порядок и цвета выводимых графиков следующие: метки времени (синий), уровень запуска (красный, если включен ждущий режим), аналоговые сигналы A0-A5, соответственно (многоцветные).
Код Oscilloscope.ino
#define ul unsigned long
#define armed true // состояние запуска и кнопки
#define continuous true // режим развертки (непрерывный)
#define falling false // запуск по спаду
#define rising true // запуск по фронту
#define triggered false // режим развертки (развертка стартует, когда происходит срабатывание)
#define sweeping false // развертка для TriggeredSweepInterval миллисекунд
#define superimposed true // сигналы накладываются на экране
#define channeled false // сигналы разделяются на каналы на экране
// настраиваемые переменные
int Beams = 6; // количество каналов чтения
bool DisplayMode = channeled; // 'superimposed' или 'channeled'
ul SampleInterval = 200; // единицы: миллисекунды * 10; 0.1 <= SampleInterval
bool SweepMode = triggered; // 'continuous' или 'triggered'
ul TriggeredSweepInterval = 40000; // общее время развертки, единицы: секунды * 10,000
float TriggerDirection = rising; // 'rising' или 'falling'
float TriggerVolts = 3.5; // постоянное напряжение запуска; 0 <= TriggerVolts <= 5
// переменные, контролируемые прерыванием
ul LastSample = -1; // первоначально последнего отсчета нет
bool LED = HIGH;
int BlinkCount = 0;
ul SweepCount = 0; // отсчитывает интервал развертки Sweep Interval
bool Tick = false; // устанавливает в true, когда TickCount = SampleRate
ul TickCount = 0; // отсчитывает SampleRate
bool TimingMark; // переключается каждый 10-ый 'Sample Interval'
ul TimingMarkCount = 0; // отсчитывает SampleInterval * 10
bool TriggerState; // 'armed' или 'sweeping'
bool TriggerOnset = false; // отмечает первую метку после срабатывания триггера
// переменные функции цикла
float ChannelFloor;
float ChannelHeight;
bool freeze = false; // true: стоп; false: запуск
int PBPin = 12; // кнопка между землей и выводом 12 (опционально)
int PBLastState = HIGH; // LOW (нажата) или HIGH (отпущена)
int PBVal; // обратная логика, кнопка подтянута к земле
float ChannelScale; // пропорция отображения сигнала
float TriggerDisplay; // вертикальное положение напряжения запуска (триггера)
int TriggerLevel; // рассчитывается из 'TriggerVolts'
float Value;
void interruptSetup()
{
noInterrupts(); // генерировать прерывание каждые 0,1 миллисекунды
TCCR2A = 2; // очистить таймер при сравнении, OCR2
TCCR2B = 2; // 16,000,000 Гц/8=2,000,000 Гц; T2 выдает импульсы каждые 0.0005 мс
OCR2A = 199; // прерывание = 0.0005*200 = 0.1 мс
TIMSK2 = 2; // установить обработчик на вектор COMPA
interrupts();
}
ISR(TIMER2_COMPA_vect) // прерывание каждые 0,1 миллисекунды
{
if (TickCount < SampleInterval)
{
TickCount++;
BlinkCount++;
if (BlinkCount >= 500)
{
BlinkCount = 0.0;
LED = !LED; // переключать светодиод каждые 50 мс
}
}
else // 'SampleInterval' истек
{
Tick = true;
TickCount = 0;
TimingMarkCount++; // обновить временную метку
if (TimingMarkCount >= 10) // произошел 10-ый 'SampleInterval'
{
TimingMark = !TimingMark;
TimingMarkCount = 0;
}
if (SweepMode == triggered)
{
if (TriggerState == sweeping) // развертка, обновить время развертки
{
SweepCount += SampleInterval;
if (SweepCount >= TriggeredSweepInterval) // развертка завершена
{
TriggerState = armed;
LastSample = -1;
}
}
else // включен, ждать триггера
{
Value = analogRead(A0);
if (LastSample > 0 and
((TriggerDirection == rising and Value >= TriggerLevel and LastSample < TriggerLevel) or
(TriggerDirection == falling and Value <= TriggerLevel and LastSample > TriggerLevel)))
{
TriggerState = sweeping; // триггер сработал
SweepCount = 0;
TriggerOnset = true;
TimingMarkCount = 0;
TimingMark = true;
}
LastSample = Value;
}
}
}
}
void setup()
{
pinMode (LED_BUILTIN, OUTPUT);
pinMode(PBPin, INPUT); // подлкючен к кнопке, замыкающей на землю
digitalWrite(PBPin, HIGH); // включить подтягивающий резистор на выводе кнопки
Serial.begin(115200);
if (SweepMode == continuous)
{
TriggerState = sweeping;
}
else
{
TriggerState = armed;
}
TriggerLevel = (TriggerVolts / 5.0 ) * 1023;
ChannelHeight = 5.0 / Beams;
ChannelScale = 5.0 / 1024.0 / Beams;
TriggerDisplay = TriggerVolts * ChannelScale + 5.0 - ChannelHeight;
interruptSetup();
}
void loop()
{
if (freeze)
{
digitalWrite(LED_BUILTIN, LOW);
}
else if (TriggerState == armed)
{
digitalWrite(LED_BUILTIN, LED);
}
else
{
digitalWrite(LED_BUILTIN, HIGH);
}
PBVal = digitalRead(PBPin);
if (PBVal == LOW and PBLastState == HIGH) // спад
{
freeze = !freeze;
delay (2); // игнорировать дребезг контактов
}
PBLastState = PBVal;
if (!freeze and TriggerState == sweeping)
{
if (Tick) // выполнить выборку, если Sample Interval миллисекунд истекли
{
if (TimingMark) // отобразить временные метки и напряжение запуска, если оно есть
{
if (TriggerOnset)
{
Serial.print(5.0);
TriggerOnset = false;
}
else
{
Serial.print(0.1);
}
}
else
{
Serial.print(0.0);
}
Serial.print(" ");
// отобразить уровень запуска, если он применяется
ChannelFloor = 5.0 - ChannelHeight;
if (SweepMode == triggered)
{
Serial.print(TriggerLevel * ChannelScale + ChannelFloor);
Serial.print (" ");
}
// выполнить выборку 1-6 аналоговых сигналов и отобразить их
for (int AnalogPin = 0; AnalogPin <= Beams - 1; AnalogPin++)
{
Value = analogRead(AnalogPin);
Value = Value * ChannelScale + ChannelFloor;
Serial.print(Value);
Serial.print(" ");
ChannelFloor -= ChannelHeight;
}
Tick = false;
Serial.println("");
}
}
}