Оптимизация этого кода C (AVR) - PullRequest
10 голосов
/ 13 апреля 2011

У меня есть обработчик прерываний, который просто не работает достаточно быстро для того, что я хочу сделать. По сути, я использую его для генерации синусоидальных волн путем вывода значения из справочной таблицы в PORT на микроконтроллере 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 выборок, а затем переменная ampitudePlace будет продолжать подсчитывать, так что, когда она достигнет 2 ^ n, она переполнится и обнулится.

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

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

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

Ответы [ 6 ]

6 голосов
/ 13 апреля 2011

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

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 после ДОБАВЛЕНИЯ.

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

Общие сведения

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

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

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

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

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

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

3 голосов
/ 13 апреля 2011

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

3 голосов
/ 13 апреля 2011

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

2 голосов
/ 13 апреля 2011

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

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

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

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

PORTD = amplitudes13[amplitudePlace++];

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

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

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

1 голос
/ 14 апреля 2011

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

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

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

0 голосов
/ 13 апреля 2011

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

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

        amplitudePlace = (amplitudePlace + 1) % numOfAmps;
}

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

...