Оптимизация этого кода C (AVR)

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

это код C:

const uint8_t amplitudes60[60] = {127, 140, 153, 166, 176, 191, 202, 212, 221, 230, 237, 243, 248, 251, 253, 254, 253, 251, 248, 243, 237, 230, 221, 212, 202, 191, 179, 166, 153, 140, 127, 114, 101, 88, 75, 63, 52, 42, 33, 24, 17, 11, 6, 3, 1, 0, 1, 3, 6, 11, 17, 24, 33, 42, 52, 63, 75, 88, 101, 114};
const uint8_t amplitudes13[13] = {127, 176,  221, 248,  202, 153, 101, 52, 17,  1, 6,  33,  75};
const uint8_t amplitudes10[10] = {127, 176,   248,  202, 101, 52, 17,  1,  33,  75};

volatile uint8_t numOfAmps = 60;
volatile uint8_t *amplitudes = amplitudes60;
volatile uint8_t amplitudePlace = 0; 

ISR(TIMER1_COMPA_vect) 
{
    PORTD = amplitudes[amplitudePlace];

    amplitudePlace++; 

    if(amplitudePlace == numOfAmps)
    {
        amplitudePlace = 0;
    }

}

амплитуды и numOfAmps изменяются другой процедурой прерывания, которая работает намного медленнее, чем эта (она в основном выполняется для изменения частот, которые воспроизводятся). В конце концов, я не буду использовать эти точные массивы, но это будет очень похожая настройка. Скорее всего, у меня будет массив с 60 значениями, А другой-только с 30. Это потому что я строю частотный метельщик, и на более низких частотах я могу позволить себе дать ему больше образцов, поскольку у меня больше тактовых циклов, чтобы играть, но на более высоких частотах я очень привязан ко времени.

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

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

Так что код там при моделировании занимает 65 циклов для завершения. Опять же, мне сказали, что я должен быть в состоянии чтобы получить его до около 30 циклов в лучшем случае.

это код ASM, созданный, с моим мышлением о том, что каждая строка делает рядом с ним:

ISR(TIMER1_COMPA_vect) 
{
push    r1
push    r0
in      r0, 0x3f        ; save status reg
push    r0
eor     r1, r1      ; generates a 0 in r1, used much later
push    r24
push    r25
push    r30
push    r31         ; all regs saved


PORTD = amplitudes[amplitudePlace];
lds     r24, 0x00C8     ; r24 <- amplitudePlace I’m pretty sure
lds     r30, 0x00B4 ; these two lines load in the address of the 
lds     r31, 0x00B5 ; array which would explain why it’d a 16 bit number
                    ; if the atmega8 uses 16 bit addresses


add     r30, r24            ; aha, this must be getting the ADDRESS OF THE element 
adc     r31, r1             ; at amplitudePlace in the array.  

ld      r24, Z              ; Z low is r30, makes sense. I think this is loading
                            ; the memory located at the address in r30/r31 and
                            ; putting it into r24

out     0x12, r24           ; fairly sure this is putting the amplitude into PORTD

amplitudePlace++; 
lds     r24, 0x011C     ; r24 <- amplitudePlace
subi    r24, 0xFF       ; subi is subtract imediate.. 0xFF = 255 so I’m
                        ; thinking with an 8 bit value x, x+1 = x - 255;
                        ; I might just trust that the compiler knows what it’s 
                        ; doing here rather than try to change it to an ADDI 

sts     0x011C, r24     ; puts the new value back to the address of the
                        ; variable

if(amplitudePlace == numOfAmps)
lds     r25, 0x00C8 ; r24 <- amplitudePlace
lds     r24, 0x00B3 ; r25 <- numOfAmps 

cp      r24, r24        ; compares them 
brne    .+4             ; 0xdc <__vector_6+0x54>
        {
                amplitudePlace = 0;
                    sts     0x011C, r1 ; oh, this is why r1 was set to 0 earlier
        }


}

pop     r31             ; restores the registers
pop     r30
pop     r25
pop     r24
pop     r19
pop     r18
pop     r0
out     0x3f, r0        ; 63
pop     r0
pop     r1
reti

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

моя единственная другая мысль-возможно, оператор if можно было бы избавиться, если бы я мог решить, как получить N бит int datatype в C, чтобы число обернулось вокруг когда он дойдет до конца? Под этим я подразумеваю, что у меня будет 2^n - 1 выборки, а затем переменная amplitudePlace просто будет продолжать подсчитывать, так что, когда она достигнет 2^n, она переполнится и будет сброшена до нуля.

Я попытался имитировать код без бит if полностью, и в то время как он улучшил скорость, он взял только около 10 циклов, так что он был примерно на 55 циклов для одного выполнения, которое все еще не достаточно быстро, к сожалению, поэтому мне нужно оптимизировать код еще больше что трудно, учитывая, что без этого это всего лишь 2 строки!!

моя единственная настоящая мысль-посмотреть, могу ли я хранить статические таблицы поиска где-нибудь, где требуется меньше тактов для доступа? Инструкции LDS, которые он использует для доступа к массиву, я думаю, что все занимают 2 цикла, поэтому я, вероятно, не буду экономить много времени, но на этом этапе я готов попробовать что угодно.

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

6 ответов


Я вижу несколько областей для начала работы, перечисленных в определенном порядке:

1. уменьшите количество регистров для нажатия, так как каждая пара push/pop занимает четыре цикла. Например, avr-gcc позволяет удалить несколько регистров из его распределителя регистров, поэтому вы можете просто использовать их для переменных регистра в этом одном ISR и убедиться, что они все еще содержат значение с прошлого раза. Вы также можете избавиться от нажатия r1 и eor r1,r1 если ваша программа не заходит r1 только 0.

2. используйте локальную временную переменную для нового значения индекса массива, чтобы сохранить ненужную нагрузку и сохранить инструкции для этой изменчивой переменной. Что-то вроде этого:--14-->

volatile uint8_t amplitudePlace;

ISR() {
    uint8_t place = amplitudePlace;
    [ do all your stuff with place to avoid memory access to amplitudePlace ]
    amplitudePlace = place;
}

3. обратный отсчет от 59 до 0 вместо от 0 до 59, чтобы избежать отдельной инструкции сравнения (сравнение с 0 происходит в любом случае при вычитании). Псевдо код:

     sub  rXX,1
     goto Foo if non-zero
     movi rXX, 59
Foo:

вместо из

     add  rXX,1
     compare rXX with 60
     goto Foo if >=
     movi rXX, 0
Foo:

4. возможно, используйте указатели и сравнения указателей (с предварительно вычисленными значениями!) вместо индексов массива. Его нужно проверить против подсчета назад, какой из них более эффективен. Возможно, выровнять массивы до 256 байтовых границ и использовать только 8-битные регистры для указателей, чтобы сэкономить на загрузке и сохранении более высоких 8 бит адресов. (Если у вас заканчивается SRAM, вы все равно можете поместить содержимое 4 из этих 60 байтовых массивов в один 256 байт массив и все еще получает преимущество всех адресов, состоящих из 8 постоянных высоких битов и 8 переменных нижних битов.)

uint8_t array[60];
uint8_t *idx = array; /* shortcut for &array[0] */
const uint8_t *max_idx = &array[59];

ISR() {
    PORTFOO = *idx;
    ++idx;
    if (idx > max_idx) {
        idx = array;
    }
}

проблема в том, что указатели 16 бит, тогда как ваш простой индекс массива ранее был 8 бит в размере. Помощь в этом может быть трюком, если вы создадите свои адреса массива так, чтобы более высокие 8 бит адреса были константами (в коде сборки,hi8(array)), и Вы имеете дело только с нижними 8 битами, которые фактически меняются в ISR. Это означает однако написание кода сборки. Сгенерированный ассемблерный код сверху может быть хорошей отправной точкой для написания этой версии РНР, в сборке.

5. если это возможно с точки зрения времени, отрегулируйте размер буфера выборки до мощности 2, чтобы заменить часть if-reset-to-zero простым i = (i+1) & ((1 << POWER)-1);. Если вы хотите пойти с 8-битным/8-битным разделением адресов, предложенным в 4., возможно, даже собирается 256 для мощности двух (и дублирование образца данные, необходимые для заполнения 256-байтового буфера), даже сохранит вам инструкцию AND после добавления.

6. в случае, если ISR использует только инструкции, которые не влияют на регистр состояния, остановите push и popping SREG.

общие

следующее может пригодиться, особенно для ручной проверки всего другого кода сборки для предположений:

firmware-%.lss: firmware-%.elf
        $(OBJDUMP) -h -S $< > $@

это создает прокомментированное полное assembly language список всего образа прошивки. Вы можете использовать это для проверки использования register (non -). Обратите внимание, что запуск кода только один раз задолго до первого включения прерываний не будет препятствовать последующему исключительному использованию регистров ISR.

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

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

Примечание не делая резервирования регистра, я получаю что-то около 31 цикла для ISR (исключая вход и выход, что добавляет еще 8 или 10 циклов). Полностью избавление от нажатия регистра приведет к снижению ISR до 15 циклов. Переход к буферу выборки с постоянным размером 256 байт и предоставлением ISR исключительного использования четырех регистров позволяет получить до 6 циклов, проводимых в ISR (плюс 8 или 10 для входа / выхода).


Я бы сказал, что лучше всего написать ISR в чистом ассемблере. Это очень короткий и простой код, и у вас есть существующий дизассемблер, чтобы направлять вас. Но для чего-то такого рода вы должны быть в состоянии сделать лучше: например, использовать меньше регистров, чтобы сэкономить на push и pop; ре-коэффициент его так, что он не загружается amplitudePlace из памяти три раза и т. д.


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


чтобы уточнить, ваше прерывание должно быть таким:

ISR(TIMER1_COMPA_vect) 
{
    PORTD = amplitudes[amplitudePlace++];
    amplitudePlace &= 63;
}

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

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

PORTD = amplitudes13[amplitudePlace++];

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

Что касается использования регистра. Как только вы получите действительно простой ISR, вы можете проверить пролог и эпилог ISR, которые толкают и всплывают состояние процессора. Если ваш ISR использует только 1 регистр, вы можете сделать это в ассемблере и сохранить и восстановить только один регистр. Это уменьшит накладные расходы прерывания, не затрагивая остальную часть программы. Некоторые компиляторы могут сделать это для тебя, но я сомневаюсь.

Если есть время и пространство,вы также можете создать длинную таблицу и заменить ++ на +=freq,где freq заставит форму волны быть целым числом, кратным базовой частоте (2x, 3x, 4x и т. д...) пропуская столько образцов.


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

в любом случае, есть сто и один способы обмана, чтобы сэкономить циклы для этого типа проблемы, если вы можете позволить себе немного согнуть ваши требования к комплекту оборудования. Например, вы можете привязать выход таймера к другому аппаратному таймеру и использовать счетчик второго таймера в качестве индекса таблицы. Вы можете зарезервировать глобальные регистры или злоупотреблять неиспользуемыми I/Os для хранения переменных. Вы можете искать две записи одновременно (или интерполировать) в прерывании COMPA и настроить крошечное второе прерывание COMPB между ними, чтобы испустить буферизованный вход. И так далее, и так далее.

с небольшим аппаратным злоупотреблением и тщательно обработанным кодом сборки вы сможете сделать это за 15 циклов или около того без особых проблем. Можно ли заставить его играть хорошо с остальной частью системы-это другой вопрос.


может быть, достаточно избавиться от условного и сравнения все вместе, используя арифметическое выражение:

ISR(TIMER1_COMPA_vect) 
{
        PORTD = amplitudes[amplitudePlace];

        amplitudePlace = (amplitudePlace + 1) % numOfAmps;
}

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