Генерация синусоидального сигнала, а следовательно и звука, на FPGA
24 января 2018
Ранее мы познакомились с IceStorm, открытым набором инструментов для разработки под FPGA серии Lattice iCE40, а также дешевой отладочной платой iCEstick на базе чипа ICE40HX1K. Кроме того, с использованием IceStorm, iCEstick и языка SystemVerilog нам удалось сделать электронные часы. Сегодня же при помощи тех же инструментов мы попробуем поработать со звуком. Однако на пути к этой благородной цели таится преграда, да не одна!
Пожалуй, главная проблема заключается в том, что для создания звука (во всяком случае, приятного) нужно генерировать честный аналоговый сигнал, не ШИМ. Чип ICE40HX1K такого не умеет, а значит понадобится ЦАП — устройство для перевода единичек и ноликов в аналоговый сигнал. Можно было бы просто найти в магазине подходящий ЦАП. Но я подумал, что намного интереснее будет спаять наш собственный из уже известных нам компонентов. Тем более, что это не так уж и трудно.
Типичный ЦАП устроен как-то так:
Иллюстрацию я позаимствовал из книги Mastering STM32 за авторством Carmine Noviello. Здесь изображен восьмибитный ЦАП. Часть слева называется R-2R лестницей. Эту схему не сложно понять, так как она представляет собой серию делителей напряжения. Если подать на все входы ЦАП нули, на выходе будет низкое напряжение. Если подать все единицы — на выходе будет максимальное напряжение VREF. Промежуточные же значения на входе приводят к промежуточным значениям на выходе. В сущности, R-2R лестница декодирует цифровой сигнал в соответствующий ему аналоговый.
Также на выходе стоит операционный усилитель, подключенный по схеме повторителя напряжения (voltage follower). В этом режиме операционный усилитель не изменяет входной сигнал, но изолирует его от нагрузки. Таким образом, сопротивление самой нагрузки не участвует в делении напряжения и не изменяет выходной сигнал ЦАП. Кстати, этот же прием можно использовать, чтобы получить в схеме положительное и отрицательное напряжение (что обычно нужно при работе со звуком) при помощи обычного делителя напряжения.
Теперь, имея самопальный ЦАП, несложно написать на SystemVerilog генератор синусоидального сигнала. Следующий код генерирует два сигнала, с частотами 678 Гц и 999 Гц, при этом сигналы переключаются один раз в секунду:
`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:
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
""");
В основном цикле получается код в стиле:
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 лестницы действительно получится достаточно точная синусоида:
Окончательный же вид устройства у меня получился таким:
Для удобства подключения 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
Метки: FPGA, Аудио, Электроника.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.