Оптимизация этого кода 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.