Насколько быстро я могу считывать данные с порта D Arduino?

Я пытаюсь узнать, насколько быстро я могу считать порт ввода-вывода Arduino. Он переключается случайной скоростью/белым прямоугольным сигналом, и я хочу узнать, что быстрее, Arduino или сигнал. Вывод на порту D управляется компаратором с открытым коллектором (LM311).

Итак, у меня есть этот код


буфер[0] = PORTD;
буфер[1] = PORTD;
буфер[2] = PORTD;
буфер[3] = PORTD;
бафф...

...fer[28] = PORTD; буфер[29] = PORTD;

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

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

Я попробовал синхронизацию buffer[0] ... buffer[29] показаний, и это возвращает 4 микросекунды с выключенными прерываниями. Надеюсь, что micros() не нуждается в прерываниях для работы! Это будет эквивалентно 7,5 миллионам показаний / сек. Это около 2 тактов на инструкцию на моей плате 16 МГц. Может ли это быть правдой?

Насколько быстро может работать этот код?

, 👍1


2 ответа


4

Ваше время кажется разумным.

По грубым подсчетам, без фактической компиляции ваш код превратится во что-то вроде этого:

IN R24,PORTD  ;First read   - temp = PORTD     - 1 cycle
ST X+, R24    ;First store  - buffer[0] = temp - 2 cycle
IN R24,PORTD  ;Second read  - temp = PORTD     - 1 cycle
ST X+, R24    ;Second store - buffer[1] = temp - 2 cycle
...

Это действительно самый быстрый способ чтения порта, если вы хотите сохранить данные. По сути, он выполнит чтение ввода-вывода из PORTD с помощью инструкции IN, что займет 1 такт. Затем он выполнит сохранение в SRAM с помощью инструкции STS, что займет еще 2 такта. Инструкция сохранения также может бесплатно увеличить указатель, который она использует (хранится в паре X-регистров R28/R29 в моем примере), что сэкономит вам время.

По сути, это должно выполнять 3 такта на каждое чтение — один для чтения и один для сохранения, или >5 миллионов считываний в секунду при тактовой частоте 16 МГц.


В начале будет дополнительно несколько циклов для установки X-регистра для указания на буфер. Если буфер является постоянным указателем, т. е. буфер имеет фиксированный адрес, то для загрузки адреса потребуется всего два цикла инструкций в начале. Если это непостоянный адрес, то потребуется до четырех, так как адрес придется загрузить из SRAM.

,

1

Я только что попробовал скомпилировать ваш код с помощью avr-gcc 4.9.2:

buffer[0]  = PORTD;
buffer[1]  = PORTD;
...
buffer[29] = PORTD;

Вот что я получил:

in  r24,    0x0b  ; temp = PORTD     – 1 cycle
sts 0x0110, r24   ; buffer[0] = temp – 2 cycles
in  r24,    0x0b  ; temp = PORTD     – 1 cycle
sts 0x0111, r24   ; buffer[1] = temp – 2 cycles
...

Это 3 цикла на чтение, т.е. частота чтения 5,33 МГц. Для по какой-то причине компилятор не захотел использовать st X+, r24 инструкция, предложенная в ответе Тома Карпентера. Давайте попробуем намекнуть немного компилятора и перепишите код на языке C следующим образом:

uint8_t * p = buffer;
*p++ = PORTD;
*p++ = PORTD;
...

Это сгенерировало точно такую же сборку! Компилятор каким-то образом выяснил адрес каждой записи памяти, и он заменил каждое вхождение указатель p по явному адресу. Чтобы предотвратить такого рода «оптимизация», сделаем указатель переменной, значение которой неизвестно во время компиляции:

void fill_buffer(uint8_t *p)
{
    *p++ = PORTD;
    *p++ = PORTD;
    ...
    *p++ = PORTD;
}

Вот сгенерированная сборка:

movw r30,  r24   ; Z = p (Z is the register pair r31:r30)
in   r24,  0x0b  ; temp = PORTD   – 1 cycle
st   Z,    r24   ; *Z = temp      – 2 cycles
in   r24,  0x0b  ; temp = PORTD   – 1 cycle
std  Z+1,  r24   ; *(Z+1) = temp  – 2 cycles
...
in   r24,  0x0b  ; temp = PORTD   – 1 cycle
std  Z+29, r24   ; *(Z+29) = temp – 2 cycles
ret              ; return

Все еще 3 цикла на чтение. Здесь компилятор использует std (сохранить со смещением) инструкция вместо st X+ (сохранить с пост-инкремент).

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

Теперь это не значит, что вы не можете читать быстрее. Ядро ЦП AVR имеет 32 регистра общего назначения. Поскольку вы выполняете только 30 чтений порта за пакет, это означает, что вы можете использовать файл регистра как сверхбыстрый временный буфер. Это, кажется, проще сделать в сборке, и это будет стоить вам значительных накладных расходов на сохранение регистров в стек и восстановление их после этого. Но сам пакет чтения будет быстрее:

; declare as:
;   extern "C" void fill_buffer(uint8_t *p);
.global fill_buffer
fill_buffer:

    ; Prologue: save registers and move the pointer.
    push r2         ; save all the registers belonging to the caller:
    push r3         ;  - 18 register to save (r2 – r17, r28, r29)
    ...
    push r28        ;  - 2 cycles per register
    movw r30, r24   ; Z = p (Z = r31:r30 is a pointer register)

    ; Now we can read the port really fast.
    in   r0,  0x0b  ; temp_0  = PORTD – 1 cycle
    in   r1,  0x0b  ; temp_1  = PORTD – 1 cycle
    ...
    in   r29, 0x0b  ; temp_29 = PORTD – 1 cycle

    ; Now save to RAM.
    st   Z+,  r0    ; *Z++ = temp_0   – 2 cycles
    st   Z+,  r1    ; *Z++ = temp_1   – 2 cycles
    ...
    st   Z+,  r29   ; *Z++ = temp_29  – 2 cycles

    ; Epilogue: restore the registers.
    pop  r28        ; restore all the previously saved registers:
    ...
    pop  r3         ;  - 18 registes to restore 
    pop  r2         ;  - 2 cycles per register
    clr  r1         ; leave r1 cleared, as required by the ABI
    ret             ; return

Теперь мы считываем порт на частоте 16 МГц: одно считывание за цикл!

Оказывается, мы можем убедить компилятор сделать именно это. Мне пришлось увидеть, чтобы поверить, но это работает. Что-то по сути эквивалентное к вышеуказанной сборке можно сгенерировать из C++ следующим образом:

// Быстро считываем порт во временные файлы.
uint8_t temp_0  = PORTD;
uint8_t temp_1  = PORTD;
...
uint8_t temp_29 = PORTD;

// Теперь сохраним в ОЗУ.
buffer[0]  = temp_0;
buffer[1]  = temp_1;
...
buffer[29] = temp_29;
,