Как соединить автономный многоканальный adc с последовательным?

Я хочу получить показания метеодатчиков и превратить 16-мегагерцевую ардуину на базе avr (у меня mega, но, похоже, это не имеет большого значения по сравнению с uno) в многоканальный осциллограф.

Сведения о задаче. Желаемая скорость передачи данных составляет от 8 (uno) до 16 (мега) аналоговых каналов, разделенных на 4–16 операций чтения в секунду. Дополнительный ацп "скоростной потенциал" надо тратить на подведение на ардуино. Предварительная шкала почти не нужна. Каждое чтение 32-битное, младший бит всегда обнуляется. Кроме того, между пакетами есть 0xffffffff (самый простой бинарный прототип, я действительно ненавижу эти низкокачественные медленные ascii, но это личное и оффтоп). Итак, теперь у вас есть установка и цель, чтобы не переходить к моему простому наброску

// определяет для установки и очистки битов регистра
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif
#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif


// пакет начинается после индикатора и идет до следующего такого индикатора.
const long packetStartIndicator = 0xFFFFFFFFl;

// значения представляют собой целые числа с фиксированной точкой 0..65534 (с младшим битом всегда очищается)
// такое решение принято потому, что младший бит почти всегда == шум;
// также это самая простая схема разбиения пакетов
const long packetValueMask = 0xFFFFFFFEl;

const int numPins = 8;

long sensorValues[numPins+1];

// TODO: настроить это с хоста через последовательный порт (внутри установки)
int pinNumbers[] = {A0, A1, A2, A3, A4, A5, A6, A7, A8};
//int pinNumbers[] = {A0, A1, A2, A3, A4, A5, A6, A7, A8, A9, A10, A11, A12, A13, A14, A15};


// 1 чтение — это 10 бит. мы можем сделать 2 ^ 6 == 64 чтения и суммировать их (дополнить до полных 16 бит, чтобы сделать цифровую экспозицию)
// обд. 1 чтение считается как 16 бит, для удобства хоста (независимо от реального АЦП)
// суммируем как 256 16-битных показаний, чтобы получить 24-битное значение. отправить как 32-битную переменную.
const int adcOrder = 10;
const int readingsOrder = 8;
const int targetOrder = 24;
const int numReadings = 1 << readingsOrder; //64;


void setup() {
  // TODO: сбросить ADCSRA, иначе это не сработает. Также: получить подсказку о режиме свободного запуска
// частота дискретизации [тактовая частота АЦП] / [предварительный делитель] / [тактовая частота преобразования]
// для Arduino Uno тактовая частота АЦП составляет 16 МГц, а преобразование занимает 13 тактовых циклов
//ADCSRA |= (1 << ADPS2) | (1<<ADPS0); // 32 предделителя для 38,5 кГц
//ADCSRA |= (1 << ADPS2); // 16 предделителей для 76,9 кГц
//ADCSRA |= (1 << ADPS1) | (1<<ADPS0); // 8 предделителей для 153,8 кГц


//sbi(ADCSRA,ADPS2); cbi(ADCSRA,ADPS1); sbi(ADCSRA,ADPS0); // установите предварительную шкалу на 32 для 38,5 кГц
//sbi(ADCSRA,ADPS2); cbi(ADCSRA,ADPS1); cbi(ADCSRA,ADPS0); // установите предварительную шкалу на 16 для 76,9 кГц
//cbi(ADCSRA,ADPS2); sbi(ADCSRA,ADPS1); sbi(ADCSRA,ADPS0); // установите предварительную шкалу на 8 для 153,8 кГц


  sensorValues[numPins] = packetStartIndicator;
  //аналоговое разрешение чтения (10);
  Serial.begin(115200, SERIAL_8N1);
  //Serial.begin(250000, SERIAL_8N1);
  //Serial.begin(500000, SERIAL_8N1);
}


void zero_values() {
  int i;
  for(i=0; i<numPins; i++) {
    sensorValues[i] = 0;
  }
}

void clear_lsb() {
  int i;
  int shiftOrder = targetOrder - (adcOrder + readingsOrder);
  for(i=0; i<numPins; i++) {
    sensorValues[i] <<= shiftOrder;
    sensorValues[i] &= packetValueMask;
  }
}

void serial_write() {
  Serial.write( (uint8_t*)sensorValues, sizeof(long)*(numPins+1) );
}

void loop() {
  int i, j; 
  zero_values();
  for(i=0; i<numPins; i++) {
    for (j=0;j<numReadings; j++) {
      sensorValues[i] += analogRead(pinNumbers[i]);
    }
  }
  clear_lsb();
  serial_write();
}

Я не умею читать спецификации avr и не разбираюсь во внутреннем устройстве. Мой вопрос связан с моим желанием перевести его в режим фрирана. Мне очень не нравится AnalogRead, кажется, его писали настоящие джуниоры, и он очень медленный. Режим свободного запуска AFAIK - это то, когда у вас есть функция прерывания, когда выполняется преобразование. Я чувствую (не знаю, но действительно чувствую), что работа с последовательным внутренним прерыванием - плохая идея. Как, говоря простыми словами, это можно переработать в ISR на основе событий с отправкой по последовательному каналу, когда вы встретите в своем прерывании следующее: currentPin == lastPin && currentReading == numReading означает, что вы суммировали желаемое количество показаний, и это последний вывод, поэтому пришло время опубликовать его по серийному номеру.

, 👍0

Обсуждение

Не могли бы вы объяснить нам, какие части AnalogRead вы считаете «младшими»., @Delta_G


1 ответ


Лучший ответ:

1

В среде Arduino отправка данных по последовательному порту внутри прерывания редко бывает хорошей идеей. Это не значит, что это невозможно, просто нужно помнить об определенных предостережениях.

Главная из которых заключается в том, что когда буфер TX заполнится, все заблокируется.

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

И да, analogRead(), как и многие другие функции в Arduino API, не работают быстро. Это не предназначено, чтобы быть быстрым. Это, однако, предназначено, чтобы быть простым. А также, как и многие вещи в Arduino API, их легко перерасти.

Я не знаком с регистрами АЦП Atmel (сейчас предпочитаю PIC32 AVR...), но глядя на техническое описание, я вижу несколько интересных моментов.

  • Сначала установите источник триггера на "свободный запуск" (ADCSRB):
  • Бит 2:0 — ADTS2:0: источник автоматического запуска АЦП

Если ADATE в ADCSRA равен единице, значение этих битов определяет, какой источник будет запускать преобразование АЦП. Если ADATE сброшен, настройки ADTS2:0 не будут действовать. Преобразование будет вызвано нарастающим фронтом выбранный флаг прерывания. Обратите внимание, что переключение с очищенного источника запуска на установленный источник запуска приведет к возникновению положительного фронта сигнала запуска. Если ADEN в ADCSRA установлен, это запустит преобразование. Переключение в режим Free Running (ADTS[2:0]=0) не вызовет триггерного события, даже если установлен флаг прерывания ADC.

Затем настройте его на использование источника триггера для запуска конверсий (ADCSRA):

  • Бит 5 — ADATE: включение автоматического запуска АЦП

Когда этот бит записывается в единицу, включается автоматический запуск АЦП. АЦП начнет преобразование при положительном фронт выбранного триггерного сигнала. Источник запуска выбирается установкой битов выбора запуска АЦП, ADTS в АДКСРБ.

И включить прерывания (ADCSRA):

  • Бит 3 — ADIE: разрешение прерывания от АЦП

Когда этот бит записывается в единицу и бит I в SREG установлен, активируется прерывание завершения преобразования АЦП

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

,

* «если вы можете гарантировать, что все ваши данные из одного прерывания будут полностью отправлены до того, как произойдет следующее прерывание» * Я могу просто гарантировать, что для 11500 бод / с == 359,375 длинных с / секунду. Я могу отправить каждый длинный, когда он запущен, и мне нужно переключиться на другой * отправить накопленный A0-> отправить A1-> ..-> отправить A7-> отправить 0xffffffff-> отправить A0 * Так что это ЛУЧШИЙ и самый легкий, если бы я мог отправить внутреннее прерывание., @xakepp35

Таким образом, я могу запустить 39,93 из этих (8 + 1) x «длинных» показаний в секунду. Мне просто нужно от 4 до 16 раз в секунду. Так что гарантировано, что последовательная шина свободна от 30% до 90% времени для отправки всего, что я хочу. Я не могу гарантировать реализацию, и я этого не знаю, и .. (я не хочу этого знать, я хочу просто использовать его эффективно) Поэтому я спросил, как запустить что-то из ISR, не ломая arduino к чертям., @xakepp35

Серийный код довольно эффективен. Существует циклический буфер TX, и отправка из этого буфера управляется прерываниями. write() блокируется, если в буфере нет места, и если это происходит в прерывании, то вы облажались. Пока вы убедитесь, что этого не может произойти (менее 64 байтов, записанных между каждым прерыванием АЦП, которое записывает данные в последовательный порт), все будет в порядке., @Majenko

Итак, ВЫ убеждаете меня, что написать send.serial.tx из какого-нибудь isr просто и достаточно просто? (если я гарантирую, что скорость отправки данных достаточно низкая? (в какой степени это должно быть хорошо? 0,9 полосы пропускания == хорошо? 0,3 == хорошо? или что? как быстро я должен отправлять, чтобы все было хорошо и не копаться в данных? я не хочу, я просто знаю, что это сразу и я иду с этим!)), @xakepp35

Если это сработает, я соглашусь. Вот просто практический вопрос к теоретикам, так в какой степени гарантирована отправка серийного номера из isr, или я должен сделать какой-то таймер, синхронизацию и еще один циклический шиткод? (чувствовалось, что нет. Я посылаю последовательный порт из прерывания, нет необходимости в каком-либо другом цикле, довольно прямолинейный вкус!) Также, если это решение без цикла АЦП, не могли бы вы написать новичкам Arduino, что они должны опубликовать в своем основном? спать? почему? не могли бы вы опубликовать код plxz !?, @xakepp35

Если вам нечего делать в loop(), то ничего не делайте в loop(). Ничего особенного делать не нужно, просто оставьте поле пустым. Существует небольшая задержка между отправкой каждого байта, так как ISR извлекает и ставит в очередь следующий байт, поэтому при скорости 115200 бод вы можете отправлять примерно 11000 байтов в секунду. Если вы отправляете 8 байтов, то 1375 отправок — это ваш абсолютный предел. Но вы хотите дать себе немного свободы действий, чтобы быть в безопасности. Таким образом, запуск не более 1000 выборок в секунду был бы правильным., @Majenko

У atmega есть какая-то операция сна? На обычном ПК, если вы «ничего не делаете в цикле», он потребляет 100% ядра и тратит энергию впустую. это гораздо сложнее, и вы должны дать планировщику ОС подсказку, чтобы перевести ядро в «спящий режим, пока не произойдет прерывание». Здесь есть что- то подобное?, @xakepp35

@ xakepp35 У него есть «спящие» режимы, но они предназначены для другой цели. Нет понятия "ничего не делать". Он всегда что-то делает, если только чип не усыплен, когда ничего (или очень мало) не происходит, пока какой-нибудь внешний стимул не разбудит его., @Majenko