Производительность АЦП AVR: прерывания и ручное преобразование

У меня есть устройство на микроконтроллере ATMega16, которое должно постоянно отправлять результаты измерений АЦП через USART. Контроллер работает на частоте 16 МГц с внешним кристаллом, а предварительный делитель АЦП установлен на 128. Я пробовал два метода преобразования АЦП и отправки результатов.

  1. Первый метод основан на прерываниях.
    int main(void) {
        ...
        while (true) {}
    }

    ISR(ADC_vect) {
        USARTSendByte(ADCL);
        USARTSendByte(ADCH);
        ADCSRA |= 1 << ADSC;
    }
  1. Второй способ основан на ручном запуске конвертации
    int main(void) {
        ... 
        // main loop
        while (true) {
            if (adcEnabled) {
                ADMUX |= channel;
                ADCSRA |= (1 << ADSC);
                while (!(ADCSRA & (1 << ADIF))) {
                    // Do nothing
                }
                ADCSRA |= (1 << ADIF); // Clear ADIF            
                USARTSendByte(ADCL);
                USARTSendByte(ADCH);
            }
        }
    }

Я выполнил ряд тестов, которые состояли из отправки 32 блоков по 512 байт (всего 16384 байта) через USART и измерения времени передачи. В первом случае (прерывания) среднее время составило 1623.13ms. Наименьший результат составил 1535 мс, а наибольший — 1712 мс. Во втором случае результат был на 1600.38ms22,75 мс меньше, чем в предыдущем случае, с наименьшим результатом 1530 мс и наибольшим результатом 1679 мс.

Итак, вопрос: действительно ли прерывания снижают производительность АЦП и почему это происходит, или результаты моих тестов были неубедительными?

обратите внимание, что вы можете проверить наличие флага ADSC в ADCSRA - микро меняет его на 0 после завершения преобразования, таким образом вам не нужно очищать флаг прерывания.

Ответы (3)

То, что вы, вероятно, измеряете, - это накладные расходы на вход и выход из ISR в программном обеспечении. Поскольку вы не декорировали ISR, он сохраняет все состояние регистра и восстанавливает его по возвращении. Ваш ISR может быть объявлен голым, и вы можете самостоятельно управлять сохранением SREG и других важных регистров (например, на встроенном ассемблере), и это приведет вас к тому, чего вы хотите.

Вы также не хотите вызывать функции из ISR. Просто переместите байты в глобальный буфер и сделайте что-нибудь с буферизованными данными из вашего основного цикла (например, отправьте их через UART).

Из руководства avr-gcc :

определить ISR_NAKED

ISR создается без кода пролога или эпилога. Пользовательский код отвечает за сохранение состояния машины, включая регистр SREG, а также за размещение reti() в конце процедуры обработки прерывания.

Спасибо, к сожалению, у меня нет опыта сборки AVR, поэтому, думаю, мне придется найти дополнительную информацию о сохранении регистра sreg.
достаточно просто написать минимальный пример на C, скомпилировать его, посмотреть на сгенерированный листинг сборки и скопировать его в обработчик прерывания «дословно», дополненный минималистским прологом и эпилогом.

Планируете ли вы делать что-то еще в микропроцессоре, кроме отправки показаний АЦП? Если нет, забудьте об использовании прерываний и просто используйте все в основном цикле. Прерывания действительно полезны, только если вы пытаетесь сделать что-то еще в «фоновом режиме».

«Прерывания действительно [...] полезны, если вы пытаетесь сделать что-то еще в «фоновом режиме»… или если вы хотите / должны использовать функции энергосбережения.

Не утруждайте себя (пока) использованием ассемблера. Вам это не нужно.

Однако важный (общий) момент: не вызывайте блокирующие функции из ISR! ISR никогда не должен ждать чего-либо .

Викатку прав: используйте какой-то «буфер», который может быть таким же простым, как одна глобальная uint16_tпеременная, в которую ISR записывает свой результат, а основной цикл извлекает его, когда может.

Еще одно замечание: в своем коде вы получаете результат ADC, отправляете его через USART, а затем начинаете следующее преобразование. Вы должны иметь возможность получить результат АЦП, начать следующее преобразование и только потом передавать данные, в то время как АЦП уже обрабатывает следующую выборку в это же время. Или вы можете посмотреть на «автономный» режим АЦП, где он автоматически запускает следующее преобразование, как только завершится предыдущее.

Чтобы проиллюстрировать, как может быть реализован общий подход к буферизации, это может служить примером:

volatile uint16_t adcValueBuffer;
volatile uint8_t adcValueBufferValidFlag;

ISR(ADC_vect) {

    adcValueBuffer = ADC;

    adcValueBufferValidFlag = 1; // This signals that the ADC provided a new value for the code outside the ISR.

    ADCSRA |= 1 << ADSC;

}


int main() {
  uint16_t adcValueCache; // Local variable which will hold the ADC value until it is completely transmitted.

  while( true ) {

    // Wait for the ISR to signal that a new value is available:
    while ( adcValueBufferValidFlag == 0 ) {
    }

    adcValueBufferValidFlag = 0; // Re-set flag. Will be set again by the ISR when a new ADC value becomes available.

    // Make sure that we read the buffered value atomically:
    cli();

    adcValueCache = adcValueBuffer;

    sei();

    USARTSendByte( (uint8_t)adcValueCache );
    USARTSendByte( (uint8_t)(adcValueCache >> 8));

  }

}

Если/когда АЦП постоянно производит выборку быстрее, чем USART может передавать данные, невозможно не потерять выборки. В этом случае вам нужно будет синхронизировать оба процесса (выборку и передачу), как вы это уже делали, начиная следующую выборку только после завершения (более медленной) операции передачи.

Кроме того, очереди или циклические буферы (также известные как кольцевые буферы ) часто используются для обмена данными с различной скоростью между асинхронными компонентами программы: ISR добавляет данные в буфер, а затем основной цикл берет (и удаляет) как можно больше элементов. как доступны в буфере, когда у него есть время.

Эти буферы могут быть реализованы в двух вариантах относительно условий переполнения: один тип отбрасывает любые новые данные, когда буфер заполнен, другой просто перезаписывает самые старые данные в буфере.

Если это то, что вам нужно, просто введите в поиск «avr round buffer»; существует множество реализаций.

Вызов функции из ISR сам по себе неплох, но функция не должна работать дольше, чем, скажем, несколько микросекунд. Простое правило таково: код, запускаемый в ISR, должен быть как можно короче. -- Вы не должны отключать прерывания во время передачи через USART, но, и я думаю, что это ваша точка зрения, вы должны отключать прерывания при чтении данных, предоставленных ISR. Вы можете легко «кэшировать» эти данные, main()например:cli(); temp = adc_value; sei(); processAdcValue( temp );
Моя процедура передачи USART выглядит следующим образом: while ((UCSRA & (1 << UDRE)) == 0) {/*Do nothing*/} UDR = temp;. Поэтому, если прерывание происходит до отправки временных данных, я в конечном итоге отправлю другое значение из-за tempперезаписи. Собственно поэтому я и хотел отключить прерывания при передаче данных.
Пожалуйста, взгляните на мое редактирование.
Спасибо за пример. А также +1 за использование соглашений об именах Java :) Но у меня все еще есть еще один вопрос: обычно ADC работает быстрее, чем USART (например, ADC со скоростью 9,615 ksps против USART со скоростью 115200 бит/с). Означает ли это, что мне, вероятно, следует использовать список (т.е. массив) переменных буфера АЦП, чтобы не потерять некоторые выборки АЦП?
Добавил еще одну правку :)
большое спасибо за очень подробный и содержательный ответ! Это действительно помогло мне разобраться.
Это здорово :) Тогда вы можете принять мой ответ.
Я бы с удовольствием это сделал, но мой первоначальный вопрос был «снизят ли прерывания производительность АЦП», поэтому я думаю, что принять ответ было бы несправедливо по отношению к @vicatcu :) Вероятно, мне следовало создать еще один вопрос о буферизации значений АЦП. Но еще раз спасибо за ваше время и усилия, я очень ценю это.