|
Виртуальный осциллограф на Arduino
Превратите свой Arduino в виртуальный осциллограф с частотой дискретизации 4,8 кГц
В этом проекте для захвата и визуализации формы сигнала используется встроенный АЦП. Он поддерживает различные режимы запуска (включая внешний запуск), позволяет пользователю выбирать аналоговый входной канал, опорное напряжение и управлять контактами микроконтроллера. Приложение для ПК, используемое в этом проекте, основано на платформе с открытым исходным кодом Simple Device Model (раскрытие: это я написал). Он поддерживает как Windows, так и Linux. Проект был протестирован на плате Arduino Uno и может работать или не работать на других Arduinos на базе AVR.
Отметим, что при частоте дискретизации 4808 Гц этот проект нельзя рассматривать как замену настоящему осциллографу.
Начиная
1. Подключите Arduino к компьютеру с помощью USB-кабеля и загрузите этот эскиз с помощью Arduino IDE. Обратите внимание на имя последовательного порта, используемого платой (например, COM3 в Windows или / dev / ttyACM0 в Linux).
2. Загрузите и установите платформу Simple Device Model.
3. Запустите sdmconsole , нажмите кнопку « Открыть плагин» и выберите демонстрацию UART в качестве плагина и Arduino Uno в качестве устройства.
4. В верхнем левом углу окна sdmconsole щелкните правой кнопкой мыши элемент Arduino Uno и выберите « Подключиться» . Введите имя последовательного порта, указанное выше.
5. Осциллограф теперь должен работать на аналоговом канале A0 . Если к нему ничего не подключено, вы, вероятно, увидите сетевые помехи 50 Гц (или 60 Гц, в зависимости от того, где вы живете).
6. Подключите цифровой контакт 3 к аналоговому входу A0 . В верхнем правом области, установлен контактный 3 режим с ШИМ и Pin3 PWM значения для 100 . Дважды щелкните область просмотра, чтобы сбросить масштаб. Теперь вы должны увидеть прямоугольную форму волны ШИМ:
7. Если вы хотите получить более плавный сигнал, поместите RC-цепь между выходом и входом. Для этого подойдут резистор ~ 100 кОм и керамический конденсатор ~ 1 мкФ.
Основные характеристики
В правой верхней панели настроек можно выбрать входной канал АЦП и опорное напряжение. Встроенный датчик температуры также можно настроить как входной канал. Учтите, что этот датчик температуры не очень точен.
Размер пакета устанавливает количество выборок в отображаемой форме волны. Это также влияет на частоту обновления. Для периодических сигналов установка размера пакета в соответствии с целым числом периодов обеспечит лучший обзор даже без синхронизации.
Раздел настроек контактов позволяет вам установить состояние контактов 2-13 MCU. Возможные состояния ввод , ввод с подтяжкой , Force низким и Force максимумом . Контакты, которые поддерживают ШИМ (широтно-импульсную модуляцию), также могут быть установлены в этот режим.
Режимы визуализации
В дополнение к базовому режиму «осциллограф» sdmconsole может визуализировать данные в виде гистограммы, изображения (где яркость пикселей представляет собой примерное значение) или двоичной диаграммы (где яркость пикселей представляет собой один бит). Режим визуализации можно изменить в раскрывающемся меню « Режим» .
Вы можете перемещаться по области просмотра, перетаскивая ее, изменять вертикальный масштаб с помощью Ctrl + колесо мыши и горизонтальный масштаб с помощью Ctrl + Shift + колесо мыши. Двойной щелчок в области видового экрана сбрасывает масштаб так, чтобы он соответствовал всему изображению.
Срабатывание
Режим синхронизации устанавливает режим триггера: « Выкл.» , « Нарастающий фронт» или « Спадающий фронт» . По умолчанию осциллограф запускается тем же сигналом, который выбран в качестве аналогового входа. Его также можно запустить с помощью цифрового вывода, выбрав его в раскрывающемся списке Источник синхронизации .
Когда аналоговый вход используется в качестве источника синхронизации, осциллограф будет запускаться, когда сигнал поднимается выше или ниже уровня синхронизации (в зависимости от фронта синхронизации). Уровень синхронизации игнорируется, когда цифровой вывод используется в качестве источника синхронизации.
Смещение синхронизации устанавливает количество отображаемых выборок перед триггером.
Эта система запуска довольно проста и достаточно хорошо работает для одиночных событий и низкочастотных сигналов. Для более высокочастотных сигналов установка подходящего размера пакета иногда может дать лучшие результаты.
Под капотом
Чтобы обеспечить стабильную частоту дискретизации, сигнал дискретизируется путем перевода АЦП в режим автономной работы, в котором он постоянно выполняет преобразования и генерирует прерывания, когда данные готовы. Из каждых двух образцов используется только один из-за ограниченной пропускной способности последовательного порта. Этот подход позволяет избежать дрожания, которое могло бы присутствовать, если бы каждое преобразование было инициировано ЦП.
Осциллограф управляется с помощью его виртуального адресного пространства: запись значения в назначенный адрес регистра интерпретируется как команда или данные конфигурации. Например, запись 3 в адрес регистра 0 выбирает канал аналогового ввода A3.
Конфигурация последовательного порта: 115200 бод, 8 битов данных, 1 стоповый бит, без контроля четности, без управления потоком.
В протоколе связи используются четыре типа фреймов:
Регистр записи (ПК -> Arduino):
01010000 ADDR[7:0] DATA[7:0]
Чтение реестра (ПК -> Arduino):
01010001 ADDR[7:0]
Регистрационные данные (Arduino -> PC):
1000 DATA[7:4] 0000 DATA[3:0]
Потоковые данные (Arduino -> ПК):
11 SOP DATA[9:5] 000 DATA[4:0]
(SOP - это флаг "начала кадра").
В этом протоколе для передачи каждой выборки требуется всего 2 байта, что позволяет различать регистровые и потоковые кадры данных.
На стороне ПК протокол реализуется плагином uartdemo, который позволяет sdmconsole обмениваться данными с осциллографом. Его исходные коды поставляются с SDM (в каталоге примеров ). Плагин написан на C ++ и реализует функции для записи и чтения значений регистров и чтения потоковых данных с осциллографа.
Самый простой способ расширить набор функций осциллографа - это изменить его виртуальное адресное пространство (то есть функции writeVirtualRegister и readVirtualRegister ). Затем новые регистры могут быть добавлены в карту регистров, которая полностью доступна для редактирования пользователем (но не забудьте сохранить ее). Чтобы изменить протокол связи, вам также потребуется изменить и перекомпилировать исходники плагина uartdemo .
КОД
/*
* ADC sampling decimation factor. Only one of each DECIMATION_FACTOR
* samples will be written to the buffer. Increase this value if you
* are having problems with communication reliability.
*
* The sampling frequency (in Hz) will be:
*
* Fs = 16000000 (CPU clock frequency) / 128 (ADC prescaler) /
* 13 (cycles per conversion) / DECIMATION_FACTOR
*/
#define DECIMATION_FACTOR 2
/*
* A circular buffer to store up to 256 ADC samples. Indexes are of
* "byte" type to make buffer overrun impossible.
*/
volatile unsigned int adcBuffer[256];
volatile byte adcWriteIndex=0;
byte adcReadIndex=0;
volatile bool triggered=false;
/* Digital pin states */
byte pinState[14];
byte pinPWM[14];
/* Synchronization settings */
volatile byte syncMode=0; /* 0 - off, 1 - rising edge, 2 - falling edge */
volatile byte syncSource=0; /* 0 - analog input, >0 - digital pin */
volatile unsigned int syncLevel=512; /* signal level for the oscilloscope to trigger */
byte syncOffset=128; /* how many samples before the trigger event will be displayed */
/* Packet size setting */
unsigned int packetSize=512;
void setup() {
/*
* Configure the ADC
* ADMUX: REFS=01 (use AVCC for reference), MUX=0000 (use ADC0 input)
* ADCSRB: ADTS=000 (free running mode, the new conversion will be started
* immediately after the previous one has been completed)
* ADCSRA: ADEN=1 (enable ADC), ADSC=1 (start conversion),
* ADATE=1 (enable auto trigger), ADIE=1 (enable ADC interrupt),
* ADPS=111 (set ADC prescaler to 128)
*/
ADMUX=0x40;
ADCSRB=0;
ADCSRA=0xEF;
Serial.begin(115200);
}
void loop() {
/* Buffer for incoming data, can hold up to 3 bytes */
static byte cmdBuffer[3];
static byte cmdBytes=0;
if(Serial.available()) {
/* Process incoming data */
byte ch=Serial.read();
if(cmdBytes==0) {
/* Look for a start of a packet */
if(ch==0x50||ch==0x51) {
cmdBuffer[0]=ch;
cmdBytes++;
}
}
else {
/* Packet continuation */
cmdBuffer[cmdBytes]=ch;
cmdBytes++;
}
if(cmdBuffer[0]==0x50&&cmdBytes==3) {
/* Write register */
writeVirtualRegister(cmdBuffer[1],cmdBuffer[2]);
cmdBytes=0;
}
else if(cmdBuffer[0]==0x51&&cmdBytes==2) {
/* Read register */
byte data=readVirtualRegister(cmdBuffer[1]);
byte buf[2];
buf[0]=0x80|(data>>4); /* upper 4 bits */
buf[1]=data&0x0F; /* lower 4 bits */
Serial.write(buf,2);
cmdBytes=0;
}
}
else if(adcWriteIndex!=adcReadIndex) {
/* No incoming data, transmit data from the ADC circular buffer if present */
static unsigned int sampleCnt=0;
byte queue=adcWriteIndex-adcReadIndex;
if(sampleCnt==0&&syncMode!=0) { /* start of packet, wait for trigger */
if(queue<syncOffset) {
triggered=false;
return;
}
if(!triggered) {
if(queue>syncOffset) adcReadIndex++;
return;
}
}
unsigned int data=adcBuffer[adcReadIndex++];
byte buf[2];
buf[0]=0xC0|(data>>5); /* upper 5 bits */
if(sampleCnt==0) buf[0]|=0x20; /* start of packet mark */
buf[1]=data&0x1F; /* lower 5 bits */
Serial.write(buf,2);
if(++sampleCnt>=packetSize) sampleCnt=0;
triggered=false;
}
}
void writeVirtualRegister(byte addr,byte data) {
/* ADC input channel */
if(addr==0) ADMUX=(ADMUX&0xF0)|(data&0x0F);
/* ADC reference voltage */
else if(addr==1) ADMUX=(ADMUX&0x3F)|(data<<6);
/* Pin mode registers */
else if(addr>=2&&addr<=13) {
pinState[addr]=data;
setPinState(addr);
}
/* PWM value registers */
else if(addr>=18&&addr<=29) {
pinPWM[addr-16]=data;
setPinState(addr-16);
}
/* Synchronization settings and packet size */
else if(addr==32) syncMode=data;
else if(addr==33) syncSource=data;
else if(addr==34) syncLevel=static_cast<unsigned int>(data)<<2;
else if(addr==35) syncOffset=data;
else if(addr==36) reinterpret_cast<byte*>(&packetSize)[0]=data;
else if(addr==37) reinterpret_cast<byte*>(&packetSize)[1]=data;
}
byte readVirtualRegister(byte addr) {
if(addr==0) return ADMUX&0x0F;
else if(addr==1) return ADMUX>>6;
else if(addr>=2&&addr<=13) return pinState[addr];
else if(addr>=18&&addr<=29) return pinPWM[addr-16];
else if(addr==32) return syncMode;
else if(addr==33) return syncSource;
else if(addr==34) return static_cast<byte>(syncLevel>>2);
else if(addr==35) return syncOffset;
else if(addr==36) return reinterpret_cast<byte*>(&packetSize)[0];
else if(addr==37) return reinterpret_cast<byte*>(&packetSize)[1];
return 0;
}
void setPinState(byte pin) {
switch(pinState[pin]) {
case 0: /* input */
pinMode(pin,INPUT);
break;
case 1: /* input_pullup */
pinMode(pin,INPUT_PULLUP);
break;
case 2: /* force low */
pinMode(pin,OUTPUT);
digitalWrite(pin,LOW);
break;
case 3: /* force high */
pinMode(pin,OUTPUT);
digitalWrite(pin,HIGH);
break;
case 4: /* PWM */
analogWrite(pin,pinPWM[pin]);
break;
}
}
/* Process the ADC interrupt */
ISR(ADC_vect) {
static byte cnt=0;
static unsigned int old_sample=0;
if(++cnt==DECIMATION_FACTOR) cnt=0;
if(cnt==0) {
unsigned int sample=ADCL|(ADCH<<8);
/* Check trigger conditions */
if(syncSource==0) { /* trigger by the analog input */
if(syncMode==1&&sample>=syncLevel&&old_sample<syncLevel) triggered=true;
else if(syncMode==2&&sample<=syncLevel&&old_sample>syncLevel) triggered=true;
old_sample=sample;
}
else { /* trigger by a digital input */
unsigned int s=digitalRead(syncSource);
if(syncMode==1&&s==HIGH&&old_sample==LOW) triggered=true;
else if(syncMode==2&&s==LOW&&old_sample==HIGH) triggered=true;
old_sample=s;
}
adcBuffer[adcWriteIndex++]=sample;
}
}