← На главную

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

Ранее мы познакомились с 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