Генератор синусоидального сигнала ATmega328p, ограничение частоты 1200 Гц


В настоящее время я реализую генератор синусоиды на ATmega328p (16 МГц). Мой проект в основном основан на этой статье https://makezine.com/projects/make-35/advanced-arduino-sound-synchronous/ .

Короче говоря, у меня есть два таймера. Первый (pwm_timer) считает от 0 до 255 и устанавливает выходной контакт на основе значения в OCR2Aрегистре, который создает сигнал PWM. Второй таймер (sample_timer) использует прерывание (ISR) для изменения значения OCR2A. ISR происходит, когда значение таймера совпадает со значением в OCR1Aрегистре, после чего таймер обнуляется.

Итак, это мой код:

#include <avr/io.h>
#include <avr/interrupt.h>
#include <math.h>

/******** Sine wave parameters ********/
#define PI2 6.283185 // 2*PI saves calculation later
#define AMP 127 // Scaling factor for sine wave
#define OFFSET 128 // Offset shifts wave to all >0 values

/******** Lookup table ********/
#define LENGTH 256 // Length of the wave lookup table
uint8_t wave[LENGTH]; // Storage for waveform
static uint8_t index = 0; // Points to each table entry

/******** Functions ********/
static inline void populate_lookup_table(void);
static inline void setup_pwm_timer(void);
static inline void setup_sample_timer(void);

int main(void)
{
    populate_lookup_table();
    setup_pwm_timer();
    setup_sample_timer();

    while(1)
    {
        asm("NOP");
        asm("NOP");
        asm("NOP");
        asm("NOP");
        asm("NOP");
        asm("NOP");
        asm("NOP");
        asm("NOP");
    }
}

ISR(TIMER1_COMPA_vect) // Called when TCNT1 == OCR1A
{
     OCR2A = wave[++index]; // Update the PWM output
}

static inline void setup_pwm_timer()
{
    TCCR2A = 0;
    TCCR2B = 0;

    TCCR2A |= _BV(WGM21) | _BV(WGM20); // Set fast PWM mode
    TCCR2A |= _BV(COM2A1); // Clear OC2A on Compare Match, set at BOTTOM
    TCCR2B |= _BV(CS20); // No prescaling
    OCR2A = 127; // Initial value
    DDRB |= _BV(PB3); // OC2A as output (pin 11)
}   

static inline void setup_sample_timer()
{
    TCCR1A = 0;
    TCCR1B = 0;
    TIMSK1 = 0;

    TCCR1B |= _BV(WGM12); // Set up in count-till-clear mode (CTC)
    TCCR1B |= _BV(CS10); // No prescaling
    TIMSK1 |= _BV(OCIE1A); // Enable output-compare interrupt
    OCR1A = 40; // Set frequency
    sei(); // Set global interrupt flag
}

static inline void populate_lookup_table()
{
    // Populate the waveform table with a sine wave
    int i;
    for (i=0; i<LENGTH; i++) // Step across wave table
    {
        float v = (AMP*sin((PI2/LENGTH)*i)); // Compute value
        wave[i] = (int)(v+OFFSET); // Store value as integer
    }
}

Теоретически частота выходного сигнала должна быть равна этой формуле 16Mhz / (LOOKUP_TABLE_LENGTH * OCR1A). Итак, для OCR1A = 100мы должны получить 625Hzсинусоиду. И это верно до тех пор ~1200Hz (OCR1A = 52). После этого вне зависимости от значения OCR1Aвыходная частота остается неизменной. Вопрос в том, почему?

Думаю проблема кроется во времени выполнения ISR. Есть ли способ ускорить его, может быть, оптимизировать код? Может быть, я должен написать это на ассемблере?

Я знаю, что могу увеличить частоту, уменьшив длину справочной таблицы, но я действительно хочу остаться с 256 образцами.

Примечание. Я понимаю, что добавление некоторых asm(“NOP”)в основной цикл немного увеличивает частоту (1250 Гц). Может быть, этот while(1) тоже виноват?

1247 ГцРезультат кода выше.

Частота дискретизацииФотография показывает, что частота дискретизации правильная (16000000 / 256 = 62500).

Мой микроконтроллер "Arduino Nano3". IDE — Atmel Studio.

Спасибо за ваше время.

Обновления:

  • Я проверяю частоту с помощью DSO Shell и динамика с анализатором спектра.
  • Уменьшение значения сопротивления и емкости добавляет несколько герц, но это незначительно.
  • Дизассемблирование показывает, что ISR — это не просто MOVинструкция.
    40: {
1f.92                PUSH R1        Push register on stack 
0f.92                PUSH R0        Push register on stack 
0f.b6                IN R0,0x3F     In from I/O location 
0f.92                PUSH R0        Push register on stack 
11.24                CLR R1     Clear Register 
8f.93                PUSH R24       Push register on stack 
ef.93                PUSH R30       Push register on stack 
ff.93                PUSH R31       Push register on stack 
    41:      OCR2A = wave[++index]; // Update the PWM output
e0.91.00.01          LDS R30,0x0100     Load direct from data space 
ef.5f                SUBI R30,0xFF      Subtract immediate 
e0.93.00.01          STS 0x0100,R30     Store direct to data space 
f0.e0                LDI R31,0x00       Load immediate 
ef.5f                SUBI R30,0xFF      Subtract immediate 
fe.4f                SBCI R31,0xFE      Subtract immediate with carry 
80.81                LDD R24,Z+0        Load indirect with displacement 
80.93.b3.00          STS 0x00B3,R24     Store direct to data space 
    42: }
ff.91                POP R31        Pop register from stack 
ef.91                POP R30        Pop register from stack 
8f.91                POP R24        Pop register from stack 
0f.90                POP R0     Pop register from stack 
0f.be                OUT 0x3F,R0        Out to I/O location 
0f.90                POP R0     Pop register from stack 
1f.90                POP R1     Pop register from stack 
18.95                RETI       Interrupt return 
Уменьшите этот конденсатор до 10n и/или уменьшите сопротивление. Возможно, виноват низкочастотный RC.
Сообщение обновлено @Wossname
Сообщение обновлено @JanDorniak
Спасибо за обновление. Не думаю, что виноват ISR. Что касается ISR, то он очень легкий.
Я все еще удивлен, что ISR настолько раздут, но у меня недостаточно опыта работы с AVR, чтобы судить, нужно ли это.

Ответы (2)

Для 1200 Гц и таблицы поиска 256 у вас есть 16000000/(256*1200) = 52 цикла между прерываниями.

Если вы считаете шаги в ASM-коде прерывания, вы находитесь на самом дне, если не ниже.

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

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

Есть также одно большое ограничение, которое все еще существует: как вы можете обновить 256-шаговый ШИМ всего за 52 цикла? Даже если вы не хотите уменьшать длину справочной таблицы, многие записи в PWM фактически игнорируются.

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

Это многое объясняет, спасибо @Dorian.

Помимо того, что говорит @Dorian, обратите внимание, что вы используете таймер PWM и таймер выборки на одной частоте. У вас есть один цикл PWM каждые 256 циклов ЦП. Если вы меняете рабочий цикл ШИМ чаще, чем каждые 256 циклов ЦП, в быстром режиме ШИМ вы получите сбои/искажения на выходе.

Чтобы смягчить проблемы, на первом этапе вы можете добавить фильтр нижних частот (RC) на выходе ШИМ, чтобы создать синусоидальный сигнал с частотой x Гц из 50% ШИМ с частотой x Гц, обходя справочную таблицу. Или используйте низкочастотный фильтр с более высокой частотой и уменьшите таблицу поиска, скажем, до 4 или 8 записей, уменьшив частоту ISR до 4 или 8 раз больше выходной частоты (вместо 256 раз) и позволив фильтру сгладить переходы между шагами.

В качестве альтернативы вы можете рассмотреть микросхемы ATtiny2/4/85, которые предлагают «настоящую» быструю ШИМ с таймером, работающим на частоте до 64 МГц.