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