Генерация синусоидального сигнала, а следовательно и звука, на FPGA

24 января 2018

Ранее мы познакомились с IceStorm, открытым набором инструментов для разработки под FPGA серии Lattice iCE40, а также дешевой отладочной платой iCEstick на базе чипа ICE40HX1K. Кроме того, с использованием IceStorm, iCEstick и языка SystemVerilog нам удалось сделать электронные часы. Сегодня же при помощи тех же инструментов мы попробуем поработать со звуком. Однако на пути к этой благородной цели таится преграда, да не одна!

Пожалуй, главная проблема заключается в том, что для создания звука (во всяком случае, приятного) нужно генерировать честный аналоговый сигнал, не ШИМ. Чип ICE40HX1K такого не умеет, а значит понадобится ЦАП — устройство для перевода единичек и ноликов в аналоговый сигнал. Можно было бы просто найти в магазине подходящий ЦАП. Но я подумал, что намного интереснее будет спаять наш собственный из уже известных нам компонентов. Тем более, что это не так уж и трудно.

Типичный ЦАП устроен как-то так:

ЦАП на базе R-2R лестницы

Иллюстрацию я позаимствовал из книги Mastering STM32 за авторством Carmine Noviello. Здесь изображен восьмибитный ЦАП. Часть слева называется R-2R лестницей. Эту схему не сложно понять, так как она представляет собой серию делителей напряжения. Если подать на все входы ЦАП нули, на выходе будет низкое напряжение. Если подать все единицы — на выходе будет максимальное напряжение VREF. Промежуточные же значения на входе приводят к промежуточным значениям на выходе. В сущности, R-2R лестница декодирует цифровой сигнал в соответствующий ему аналоговый.

Также на выходе стоит операционный усилитель, подключенный по схеме повторителя напряжения (voltage follower). В этом режиме операционный усилитель не изменяет входной сигнал, но изолирует его от нагрузки. Таким образом, сопротивление самой нагрузки не участвует в делении напряжения и не изменяет выходной сигнал ЦАП. Кстати, этот же прием можно использовать, чтобы получить в схеме положительное и отрицательное напряжение (что обычно нужно при работе со звуком) при помощи обычного делителя напряжения.

Теперь, имея самопальный ЦАП, несложно написать на SystemVerilog генератор синусоидального сигнала. Следующий код генерирует два сигнала, с частотами 678 Гц и 999 Гц, при этом сигналы переключаются один раз в секунду:

/* vim: set ai et ts=4 sw=4: */
`default_nettype none

module prescaler(input logic in_clk, output logic out_clk);
    // 9600 @ 12 Mhz
    parameter counter_max = 1250;
    // = math.ceil(math.log2( counter_max ))
    parameter counter_bits = 11;

    logic [counter_bits-1:0] devider;

    always_ff @(posedge in_clk)
    begin
        if(devider == counter_max)
        begin
            devider <= 0;
            out_clk <= 1;
        end
        else
        begin
            devider <= devider + 1;
            out_clk <= 0;
        end
    end
endmodule // prescaler

module sawtooth_sig(input logic clk, output logic [0:7] sig);
    logic [0:7] counter = 0;

    assign sig = counter;

    always_ff @(posedge clk)
    begin
        if(counter == 8'b11111111)
            counter <= 0;
        else
            counter <= counter + 1;
    end
endmodule // sawtooth_sig

module top(
        input logic raw_clk,
        output logic [0:7] sig);
    logic clk678hz, clk999hz, clk_chfreq;
    logic [0:7] sig678hz;
    logic [0:7] sig999hz;
    logic use678hz = 1;

    assign sig = use678hz ? sig678hz : sig999hz;

    always_ff @(posedge clk_chfreq)
    begin
        use678hz <= !use678hz;
    end

    // 12 Mhz => 256*678 Hz
    prescaler #(.counter_max(69), .counter_bits(7))
        clk678hz_ps(
            .in_clk(raw_clk),
            .out_clk(clk678hz));

    // 12 Mhz => 256*999 Hz
    prescaler #(.counter_max(47), .counter_bits(6))
        clk999hz_ps(
            .in_clk(raw_clk),
            .out_clk(clk999hz));

    // 12 Mhz => 1 Hz
    prescaler #(.counter_max(12000000), .counter_bits(24))
        clk_chfreq_ps(
            .in_clk(raw_clk),
            .out_clk(clk_chfreq));

    sine_sig gen678hz(clk678hz, sig678hz);
    sine_sig gen999hz(clk999hz, sig999hz);
endmodule // top

Модуль sine_sig здесь не объявлен, так как он генерируется скриптом на Python:

#!/usr/bin/env python3

import math

samples = 256
scale = 127
volume = 1.0

print("""\
/* vim: set ai et ts=4 sw=4: */
`default_nettype none

module sine_sig(input logic clk, output logic [0:7] sig);
    logic [0:7] counter;

    sawtooth_sig st_sig(clk, counter);

    always_ff @(posedge clk)
    begin
        case (counter)\
"""
);

for i in range(0,samples):
    x = 2*math.pi*i/samples
    f = (" " * 12) + "8'b{:08b}: sig <= 8'b{:08b};"
    val = int(volume*(math.sin(x)*scale + scale))
    print(f.format(i, val))

print("""\
            default: sig <= 8'b00000000; // should never happen
        endcase
    end
endmodule
"""
);

В основном цикле получается код в стиле:

    always_ff @(posedge clk)
    begin
        case (counter)
            8'b00000000: sig <= 8'b01111111;
            8'b00000001: sig <= 8'b10000010;
            8'b00000010: sig <= 8'b10000101;
            8'b00000011: sig <= 8'b10001000;

При помощи осциллографа не сложно убедиться, что на выходе R-2R лестницы действительно получится достаточно точная синусоида:

Осциллограмма синусоидного сигнала

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

Генератор звука на FPGA

Для удобства подключения iCEstick я спаял пару адаптеров на базе разъема IDC-10. Соответствующий шлейф, изображенный слева на фото, идет к плате. R-2R лестницу на фото узнать не сложно. Ее выход идет на второй канал операционного усилителя MCP6142. Первый же канал используется в сочетании с делителем напряжения для получения половины от напряжения питания (1.65 В). Это напряжение используется в качестве земли для звука. Небольшой динамик на 2 Вт сопротивлением 8 Ом подключен к этой земле и выходу второго канала операционного усилителя. В результате генерируется громкий и чистый звук. Так как частота сигнала меняется раз в секунду, звук напоминает вой сирены.

Стоит отметить, что можно было использовать и уже знакомый нам операционный усилитель NE5532. Однако для штатной работы ему требуется как минимум 6 В (или, что то же самое, +/- 3В) напряжения питания. В этом же проекте у нас есть только 3.3 В. При таком напряжении со своей задачей операционный усилитель вроде справляется, но осциллографом можно увидеть, что выходной сигнал получается чуть тише, чем он должен был быть, а также заметно искаженным. В отличие от NE5532, использованный мной MCP6142 корректно работает уже при 1.4 В, а максимальное напряжение питания у него 6 В. Таким образом, он способен выдавать звук без каких-либо искажений. Распиновка же у NE5532 и MCP6142 одинаковая.

Полную версию исходников к этому посту, как обычно, я выложил на GitHub.

Раз уж речь зашла о чистоте звука, в качестве домашнего задания вы можете повысить разрядность ЦАП с 8 до 12. Для уменьшения числа проводов и использованных GPIO может иметь смысл воспользоваться сдвиговыми регистрами. А еще можно генерировать звук и поинтереснее, например, мелодию из игры Super Mario Bros или видео Nyan Cat.

Как видите, простор для творчества здесь безграничный.

Дополнение: На YouTube-канале The Signal Path было найдено совершенно замечательное видео по теме самодельных ЦАП — Tutorial on the Theory, Design and Measurement of Nyquist Digital to Analog Converters.

Дополнение: Вас также могут заинтересовать статьи Учим iCEstick передавать видео-сигнал по VGA и Подключаем плату Alinx AN108 с АЦП и ЦАП к BlackIce II

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.