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

EnglishRussianUkrainian