Оптимизация кода 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-тип данных в C, чтобы число обернулось, когда оно достигнет конца? Под этим я имею в виду, что у меня было бы 2 ^ n - 1 выборки, а затем переменная амплитуды Place просто продолжала бы подсчитывать, так что, когда она достигнет 2 ^ n, она переполнится и будет reset равна нулю.

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

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

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

Ответ 1

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

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

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

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, Это значит, что писать код сборки. Сгенерированный код сборки сверху может быть хорошей отправной точкой для написания этой версии ISR в сборке.

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

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

Общие

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

firmware-%.lss: firmware-%.elf
        $(OBJDUMP) -h -S $< > [email protected]

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

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

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

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

Ответ 2

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

Ответ 3

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

Ответ 4

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

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

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

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

PORTD = amplitudes13[amplitudePlace++];

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

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

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

Ответ 5

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

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

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

Ответ 6

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

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

        amplitudePlace = (amplitudePlace + 1) % numOfAmps;
}

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