Почему наблюдается тактовая частота < 3 МГц на Arduino Uno?

Я написал простой цикл для проверки скорости процессора на моем Arduino Uno. Цифры, которые я получаю, намного хуже, чем рекламируемые 16 МГц, примерно в 5 раз. Пытаюсь понять, что мне не хватает.

long counter = 0;
int sum = 0;
uint32_t t = 0;

void setup() {
  Serial.begin(9600);
  t = micros();
}

void loop() {
  sum += counter;

  if (counter == 10000) {
    float usecsPerIteration = (micros() - t) / (counter * 1.0);
    Serial.print(usecsPerIteration);
    Serial.println(" microseconds per iteration");

    // Выводим результат, чтобы избежать оптимизации цикла
    Serial.println(sum);
  }

  ++counter;
}

Вывод:

3.77 microseconds per iteration

Вот как я оцениваю тактовую частоту. Тело цикла включает два сложения и проверку на равенство. Даже если мы предположим, что эти простые операции занимают 10 тактов, подразумеваемая тактовая частота по-прежнему будет равна (1 иттер / 3,77 мкс) * (1e6 мкс / сек) * (10 циклов / иттер) = 2,65 МГц.

Что я упускаю?

, 👍3

Обсуждение

Вы забыли, что сама печать также вносит свой вклад в уравнение. Это ОЧЕНЬ ДОРОГО. Вы должны сбрасывать время после каждой печати., @Kwasmich

Я печатаю только один раз после 10 000 итераций, и это происходит после того, как истекло время вычислений., @rampatowl


2 ответа


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

8

По словам Майкла, происходит гораздо больше, чем вы думаете.

Во-первых, есть срабатывание прерываний, которые могут замедлить работу (используется для вычисления milllis()).

Во-вторых, вы значительно недооцениваете количество вызываемых инструкций. Для начала цикл вызывается из цикла for(;;) в main():

    for (;;) {
        loop();
4   572:    0e 94 73 00     call    0xe6    ; 0xe6 <loop>
        if (serialEventRun) serialEventRun();
2   576:    20 97           sbiw    r28, 0x00   ; 0
2   578:    e1 f3           breq    .-8         ; 0x572 <main+0x10>
-   57a:    0e 94 1d 01     call    0x23a   ; 0x23a <_Z14serialEventRunv>
2   57e:    f9 cf           rjmp    .-14        ; 0x572 <main+0x10>
10 total

Затем для вызова loop() используемые регистры помещаются в стек, затем выполняется ваш код, и регистры снова извлекаются перед возвратом. Это код цикла в разобранном виде:

000000e6 <loop>:
    // Сохраняем регистры в стек
2   e6:     cf 92           push    r12
2   e8:     df 92           push    r13
2   ea:     ef 92           push    r14
2   ec:     ff 92           push    r15
8 total

    // Добавляем к сумме
2   ee:     40 91 44 01     lds r20, 0x0144 ; 0x800144 <counter>
2   f2:     50 91 45 01     lds r21, 0x0145 ; 0x800145 <counter+0x1>
2   f6:     60 91 46 01     lds r22, 0x0146 ; 0x800146 <counter+0x2>
2   fa:     70 91 47 01     lds r23, 0x0147 ; 0x800147 <counter+0x3>
2   fe:     80 91 42 01     lds r24, 0x0142 ; 0x800142 <sum>
2   102:    90 91 43 01     lds r25, 0x0143 ; 0x800143 <sum+0x1>
1   106:    84 0f           add r24, r20
1   108:    95 1f           adc r25, r21
2   10a:    90 93 43 01     sts 0x0143, r25 ; 0x800143 <sum+0x1>
2   10e:    80 93 42 01     sts 0x0142, r24 ; 0x800142 <sum>
18 total

    // Проверяем значение, чтобы узнать, пора ли печатать
1   112:    40 31           cpi r20, 0x10   ; 16
1   114:    57 42           sbci    r21, 0x27   ; 39
1   116:    61 05           cpc r22, r1
1   118:    71 05           cpc r23, r1
2   11a:    d1 f5           brne    .+116       ; 0x190 <loop+0xaa>
6 total

    // Отсюда следует раздел "IF", пропущенный BRNE выше
-   11c:    0e 94 f5 04     call    0x9ea   ; 0x9ea <micros>
-   120:    c0 90 3e 01     lds r12, 0x013E ; 0x80013e <__data_end>
-   124:    d0 90 3f 01     lds r13, 0x013F ; 0x80013f <__data_end+0x1>
-   128:    e0 90 40 01     lds r14, 0x0140 ; 0x800140 <__data_end+0x2>
-   12c:    f0 90 41 01     lds r15, 0x0141 ; 0x800141 <__data_end+0x3>
-   130:    6c 19           sub r22, r12
-   132:    7d 09           sbc r23, r13
-   134:    8e 09           sbc r24, r14
-   136:    9f 09           sbc r25, r15
-   138:    0e 94 68 06     call    0xcd0   ; 0xcd0 <__floatunsisf>
-   13c:    6b 01           movw    r12, r22
-   13e:    7c 01           movw    r14, r24
-   140:    60 91 44 01     lds r22, 0x0144 ; 0x800144 <counter>
-   144:    70 91 45 01     lds r23, 0x0145 ; 0x800145 <counter+0x1>
-   148:    80 91 46 01     lds r24, 0x0146 ; 0x800146 <counter+0x2>
-   14c:    90 91 47 01     lds r25, 0x0147 ; 0x800147 <counter+0x3>
-   150:    0e 94 6a 06     call    0xcd4   ; 0xcd4 <__floatsisf>
-   154:    9b 01           movw    r18, r22
-   156:    ac 01           movw    r20, r24
-   158:    c7 01           movw    r24, r14
-   15a:    b6 01           movw    r22, r12
-   15c:    0e 94 c7 05     call    0xb8e   ; 0xb8e <__divsf3>
-   160:    ab 01           movw    r20, r22
-   162:    bc 01           movw    r22, r24
-   164:    22 e0           ldi r18, 0x02   ; 2
-   166:    30 e0           ldi r19, 0x00   ; 0
-   168:    88 e4           ldi r24, 0x48   ; 72
-   16a:    91 e0           ldi r25, 0x01   ; 1
-   16c:    0e 94 a9 04     call    0x952   ; 0x952 <_ZN5Print5printEdi>
-   170:    60 e0           ldi r22, 0x00   ; 0
-   172:    71 e0           ldi r23, 0x01   ; 1
-   174:    88 e4           ldi r24, 0x48   ; 72
-   176:    91 e0           ldi r25, 0x01   ; 1
-   178:    0e 94 0b 03     call    0x616   ; 0x616 <_ZN5Print7printlnEPKc>
-   17c:    60 91 42 01     lds r22, 0x0142 ; 0x800142 <sum>
-   180:    70 91 43 01     lds r23, 0x0143 ; 0x800143 <sum+0x1>
-   184:    4a e0           ldi r20, 0x0A   ; 10
-   186:    50 e0           ldi r21, 0x00   ; 0
-   188:    88 e4           ldi r24, 0x48   ; 72
-   18a:    91 e0           ldi r25, 0x01   ; 1
-   18c:    0e 94 b0 03     call    0x760   ; 0x760 <_ZN5Print7printlnEii>

 // Все это просто counter++
2   190:    80 91 44 01     lds r24, 0x0144 ; 0x800144 <counter>
2   194:    90 91 45 01     lds r25, 0x0145 ; 0x800145 <counter+0x1>
2   198:    a0 91 46 01     lds r26, 0x0146 ; 0x800146 <counter+0x2>
2   19c:    b0 91 47 01     lds r27, 0x0147 ; 0x800147 <counter+0x3>
2   1a0:    01 96           adiw    r24, 0x01   ; 1
1   1a2:    a1 1d           adc r26, r1
1   1a4:    b1 1d           adc r27, r1
2   1a6:    80 93 44 01     sts 0x0144, r24 ; 0x800144 <counter>
2   1aa:    90 93 45 01     sts 0x0145, r25 ; 0x800145 <counter+0x1>
2   1ae:    a0 93 46 01     sts 0x0146, r26 ; 0x800146 <counter+0x2>
2   1b2:    b0 93 47 01     sts 0x0147, r27 ; 0x800147 <counter+0x3>
20 total 

 // Получить регистры обратно и вернуться
2   1b6:    ff 90           pop r15
2   1b8:    ef 90           pop r14
2   1ba:    df 90           pop r13
2   1bc:    cf 90           pop r12
4   1be:    08 95           ret
12 total 

Это более 10 инструкций. Я делаю 74 такта, чтобы выполнить одну полную итерацию. (один counter++ равен 20 тактовым циклам — удвойте общую оценку...)

Вы должны помнить две важные вещи:

  • AVR – это RISC-процессор. Это означает, что на первый взгляд простые операции могут выполняться с использованием нескольких инструкций.
  • AVR — это 8-битный ЦП. Это означает, что для работы с любыми переменными размером более 8 бит требуется гораздо более сложный код, и вы работаете с 32-битными переменными (и числами с плавающей запятой, но это выходит за рамки цикла цикла синхронизации в ваш код).
,

Я насчитал 72 такта на итерацию вашей дизассемблированной версии (20 для counter++). Похоже, вы скомпилировали без -flto., @Edgar Bonet

@EdgarBonet Вполне возможно, да. Я использую UECIDE, который может не использовать те же флаги компиляции, что и Arduino, если они решат случайным образом изменить такие вещи - также, вероятно, не та же версия компилятора (у меня есть много версий на выбор)., @Majenko

@EdgarBonet ADIW - это два часа, а не один, который я вижу, хотя для этого требуется только одно чтение из флэш-памяти. Я считал количество считываний флэш-памяти как часы. Так что да, их даже больше, чем кажется на первый взгляд., @Majenko

Действительно, разработчики Arduino изменили флаги компиляции [добавили LTO в 1.6.10](https://blog.arduino.cc/2016/07/27/download-the-new-arduino-ide-1-6-10). /)., @Edgar Bonet

@EdgarBonet Я должен добавить это к ядру Arduino в UECIDE, тогда действительно. В любом случае, пора создавать новую версию. В любом случае - я перечислил (и суммировал) фактические значения тактового цикла в ответе., @Majenko

@EdgarBonet Ох ... и для системы «RISC» у AVR наверняка есть много инструкций ..., @Majenko


0

Функция цикла вызывается в среде Arduino IDE. Помимо кода, который вы пишете, также выполняется проверка прерывания, которая также требует времени и объясняет разницу.

Следующая функция МОЖЕТ вызываться после каждого вызова цикла. Подробности смотрите в ответе Маженко.

/*
  SerialEvent occurs whenever a new data comes in the hardware serial RX. This
  routine is run between each time loop() runs, so using delay inside loop can
  delay response. Multiple bytes of data may be available.
*/
void serialEvent() {
  while (Serial.available()) {
    // получаем новый байт:
    char inChar = (char)Serial.read();
    // добавляем его в inputString:
    inputString += inChar;
    // если входящий символ является новой строкой, установите флаг, чтобы основной цикл мог
    // сделать что-нибудь с этим:
    if (inChar == '\n') {
      stringComplete = true;
    }
  }
}

Для получения дополнительной информации см. SerialEvent.

Однако, как говорится в комментарии Kwasmich, вы можете легко обойти это, выполнив тест внутри цикла while.

,

Действительно ли проверка цикла прерывания занимает ~ 50 циклов?, @rampatowl

Повторите свой эксперимент, переместив код цикла в цикл while(1) {} внутри цикла setup(). Таким образом, вы можете исключить вклад фреймворка arduino., @Kwasmich

Я пытаюсь найти код., @Michel Keijzers

SerialEvent — это необязательная функция, которую пользователь может реализовать в своем скетче, если захочет. Если они не реализуют это, то ничего не вызывается. Тем не менее, он быстро проверяет, определена ли функция или нет, что занимает пару тактов., @Majenko

@Majenko ... спасибо, я обновлю свой ответ и проголосую за ваш., @Michel Keijzers