Помогите понять время выполнения AVR

Я работаю с микроконтроллером Atmel ATMEGA32U4 - таблица данных здесь с кристаллом 16 МГц для системных часов.

Насколько я понимаю, этот чип имеет запрограммированный на заводе предохранитель «Делить тактовую частоту на 8», что фактически делает мою системную тактовую частоту 2 МГц. (Страница 348 таблицы данных. CKDIV8 (бит 7) значение по умолчанию равно 0, запрограммировано).

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

Вот код, который я использовал для этого:

//Set PORT E as output
DDRE = 0xFF;

asm volatile("nop\n\t"::); 
PORTE |= 1<<2;  

asm volatile("nop\n\t"::); 
PORTE &=~ (1<<2);   

asm volatile("nop\n\t"::); 
PORTE |= 1<<2;  

asm volatile("nop\n\t"::); 
PORTE &=~ (1<<2);

«Нет» равен одному тактовому циклу, согласно руководству по набору инструкций AVR , стр. 108.

Основываясь на этой информации, я бы предположил, что инструкция «nop» занимает 500 наносекунд. Верно ли это предположение? (16 МГц/8 = 2 МГц. 1 2 М ЧАС г = 500 нс)

Вот график моих выводов:введите описание изображения здесь

Может показаться, что время выполнения «nop» составляет всего 200 нс (5 МГц). Я ожидаю, что будут некоторые дополнительные накладные расходы, используя C для установки высокого / низкого уровня порта, поэтому 500 нс на самом деле минимальное время, которое я ожидал бы, что контакт будет низким . Но, как вы можете видеть из моих измерительных курсоров «a» и «b», это даже не близко к 500 нс.

Кто-нибудь может объяснить, что происходит?
Есть ли ошибка в моем методе?
Я пропустил что-то глупое? :p
Спасибо за любую помощь!

Как вы компилируете этот код? Даже с volatile, GCC по-прежнему будет перемещать эти операторы сборки на gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html , если у вас включена оптимизация (что-либо, кроме -O0).
Скомпилировано с помощью AVR GCC, оптимизировано по размеру (-Os), который, по-видимому, является настройкой по умолчанию. Я посмотрю, что даст отключение оптимизации.
компиляция, а затем дизассемблирование вашего кода в моей голове приводит к тому, out DDRE, 0xFF; nop; sbi PORTB, 2; nop; cbi PORTB, 2; nop; sbi PORTB, 2; nop; cbi PORTB, 2;что, поскольку все это инструкции с 1 циклом, время между изменениями вывода должно составлять 2 цикла (один для nop, один для sbi/ cbi). однако ваш компилятор может оптимизировать nops или быть недостаточно умным, чтобы использовать sbiиcbi

Ответы (3)

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

Кроме того, можете ли вы просмотреть дизассемблирование вашего кода C? Я бы даже не пытался судить о времени, не увидев, какой именно ассемблерный код был сгенерирован из моего кода C. Я бы попытался работать в обратном направлении от сборки, чтобы рассчитать, каким должно быть время выполнения (с помощью руководства по набору инструкций, которое вы нашли). Таким образом, вы можете увидеть, немного или сильно отличается измерение прицела. Немного может означать, что вы сделали математическую ошибку. Многое может означать, что одно из ваших предположений неверно.

Я предполагаю, что хотел бы, чтобы число, подобное миллиону, учитывало стабильность часов (частей на миллион)? Также, хорошее предложение о взгляде на разборку. Я пока не знаю, как это сделать, но я разберусь. Тогда я точно увижу, какие инструкции выполняются.
@ dext0rb Да, это одна из веских причин. Другая причина заключается в том, что команды для переключения внешнего DIO занимают примерно столько же времени, сколько один NOP, но не столько времени, сколько один миллион. Они будут незначительными.
Спасибо всем, хотел бы я принять все ваши ответы, поскольку все они были очень хорошими, но этот заставил меня задуматься и глубже погрузиться в сборку. Я последовал предложению @noah1989 перейти на «голое железо», и это очень помогло. Я сделал 100 NOP подряд, и это равняется 50 мкс, что для меня имеет смысл.

Некоторое время назад я проверил различные настройки часов ATtiny45 с помощью приведенного ниже кода C. Хотя цикл while не идеален, процедуры задержки довольно хорошо оптимизированы с помощью библиотек avr-gcc. Я называю программу «1kHz.cpp». Проверьте таблицу данных, какой именно контакт выводит блочную волну.

#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
  DDRB = 0x10;
  PORTB = 0x00;

  while (1)
  {
    PORTB = 0x00; _delay_us(500);  // pin low, then wait
    PORTB = 0x10; _delay_us(500);  // pin high, then wait
  }
}

Хитрость с часами заключается в том, чтобы скомпилировать код с правильной настройкой часов. В Linux это выглядит так, но в Windows должно быть примерно так же. Как вы видите, ATtiny45 имеет заводскую частоту 1 МГц по умолчанию.

freq=8000000/8
baud=19200
src=1kHz.cpp
avr=attiny45
port=/dev/ttyUSB1

# Compile
avr-gcc -g -DF_CPU=$freq -Wall -Os -mmcu=$avr -c -o tmp.o $src
# Link
avr-gcc -g -DF_CPU=$freq -Wall -Os -mmcu=$avr -o tmp.elf tmp.o
# Convert to Intel .hex (required for my programmer)
avr-objcopy -j .text -j .data -O ihex tmp.elf tmp.hex
# Program the device
avrdude -p $avr -c stk500v1 -P $port -b $baud -v -U flash:w:tmp.hex

Кстати, эксперименты с настройкой часов могут привести к поломке вашего устройства, поэтому будьте осторожны, какие предохранители вы устанавливаете/сбрасываете. Разблокировку можно выполнить только с помощью высоковольтного программатора (который можно сделать с помощью запасного Arduino).

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

инструкция_часы

Однако, если вы просто хотите проверить правильность основной тактовой частоты, есть более простое решение: просто сгенерируйте низкочастотный выход. (от 0,1 Гц до нескольких кГц, в зависимости от того, что вы используете для измерения, мигающего светодиода и секундомера может быть достаточно.) Вы можете использовать для этого один из 16-битных таймеров или просто использовать цикл с длинным временем занятости. подождите, напр _delay_us(). Время, потраченное на прыжки петли, будет пренебрежимо мало.