Esta página web pertenece a un grupo de pequeños proyectos que exploran las características de la placa Smart Zynq.
Este proyecto está también publicado en HelloFPGA, un sitio recomendado para los lectores chinos.
Introducción
Este tutorial explica cómo conectar unos auriculares normales a la placa Smart Zynq y escuchar música. El propósito de este proyecto es demostrar el uso de un flujo Xillybus para el envío de datos continuos a la FPGA. También se muestra el código Verilog que implementa un modulador PWM.
El código que se muestra aquí no es un ejemplo de cómo implementar una salida de audio. El método habitual para implementar una salida analógica es una técnica más complicada, denominada Sigma-Delta. Esta técnica se puede implementar en una FPGA, pero los fundamentos teóricos son mucho más difíciles de entender.
Otro inconveniente de esta implementación es que su frecuencia de muestreo es inexacta (48828 Hz en lugar de 48000 Hz). Esto se puede corregir fácilmente cambiando la frecuencia del reloj que usa la lógica. El tema de manipular los relojes para este propósito no se muestra aquí, porque este ejemplo hace hincapié en la simplicidad más que en la exactitud.
El equipo para esta demostración es:
- Una placa Smart Zynq (SP o SL).
- Un par de auriculares analógicos normales.
- Pinzas de cocodrilo y cables, u otros medios para conectarse a los pines de la placa.
- Opcional: una resistencia de 50 Ω–200 Ω. La finalidad de esta resistencia es proteger los auriculares y la placa Smart Zynq de una corriente eléctrica excesiva. El ejemplo funcionará sin esta resistencia, pero con riesgo de dañar los componentes electrónicos.
Preparación del proyecto Vivado
Cree un nuevo proyecto Vivado a partir del archivo zip del paquete de demostración (demo bundle), es decir, el kit de la partición de arranque. Abra verilog/src/xillydemo.v en un editor de texto. Elimine la parte del código etiquetada como «PART 2». En lugar de esa parte, inserte este fragmento de código:
/*
* PART 2
* ======
*
* This code demonstrates a PWM-based audio output
*/
reg [10:0] pwm_level, threshold_left, threshold_right;
reg pwm_left, pwm_right;
reg fifo_out_valid;
wire [31:0] fifo_out;
wire fifo_empty;
wire fifo_rd_en = !fifo_out_valid && !fifo_empty;
wire next_word = (pwm_level == 11'h7ff);
assign J6 = { pwm_right, pwm_left };
always @(posedge bus_clk)
begin
pwm_level <= pwm_level + 1;
if (next_word && fifo_out_valid)
begin
// The audio samples are signed integers. Change them to
// unsigned by adding 1024.
threshold_left <= fifo_out[15:5] + 1024;
threshold_right <= fifo_out[31:21] + 1024;
end
else if (next_word) // FIFO's output not valid, keep silent
begin
threshold_left <= 0;
threshold_right <= 0;
end
pwm_left <= (threshold_left > pwm_level);
pwm_right <= (threshold_right > pwm_level);
if (fifo_rd_en)
fifo_out_valid <= 1;
else if (next_word)
fifo_out_valid <= 0;
end
// 32-bit FIFO for audio samples
fifo_32x512 fifo_32
(
.clk(bus_clk),
// Interface with Xillybus IP core
.srst(!user_w_write_32_open),
.din(user_w_write_32_data),
.wr_en(user_w_write_32_wren),
.full(user_w_write_32_full),
// Interface with application logic
.rd_en(fifo_rd_en),
.dout(fifo_out),
.empty(fifo_empty)
);
// Send the text "PWM" to reassure that the correct bitstream is used.
assign user_r_read_32_eof = 0;
assign user_r_read_32_empty = 0;
assign user_r_read_32_data = 32'h0a_4d_57_50; // "PWM" + LF
Alternativamente, descargue el archivo xillydemo.v desde aquí.
Cree un archivo de flujo de bits (bitstream) a partir del proyecto actualizado, igual que hizo con el paquete de demostración. Copie también el archivo de flujo de bits a la tarjeta TF de la misma manera (sobrescriba el antiguo xillydemo.bit con el archivo que se haya generado con este proyecto).
Conexión de los auriculares
Conecte una resistencia de 50 Ω–200 Ω al pin de E/S que lleva la señal de audio: J6/1 (canal izquierdo) o J6/2 (canal derecho). Para encontrar J6, busque en la parte trasera de la placa Smart Zynq la inscripción «Bank 33 VCCIO Vadj». La fila de pines cercana a esa marca es el conector de pines con el que trabajaremos. Por tanto, J6/1 es el pin más cercano al conector HDMI.
Conecte el otro extremo de la resistencia a la punta del conector de los auriculares. Para ello puede usar una pinza de cocodrilo.
Conecte la parte exterior del conector de los auriculares (el manguito, «sleeve» en inglés) a tierra de la Smart Zynq. La tierra del conector de pines está en J6/35 o J6/36. Sin embargo, no se recomienda usar esos pines porque están cerca de los pines de alimentación.
En su lugar, también se puede usar cualquier pin del rango de J6/3 a J6/34. La FPGA los considera pines de salida y los mantiene a un nivel lógico «0». Por tanto, es posible usarlos como tierra.
También se puede obtener la conexión a tierra conectando una pinza de cocodrilo a la parte metálica exterior de uno de los conectores de la placa: el conector Ethernet, el conector HDMI o uno de los conectores USB.
Encendido de la placa
Encienda la Smart Zynq como de costumbre (o reiníciela). El siguiente paso es asegurarse de que el archivo de flujo de bits (bitstream) correcto está cargado en la FPGA (parte PL).
Escriba el comando «head /dev/xillybus_read_32» en el indicador de shell. Este comando lee las primeras líneas de /dev/xillybus_read_32 y muestra el resultado:
# head /dev/xillybus_read_32 PWM PWM PWM PWM PWM PWM PWM PWM PWM PWM
Si este comando no produce ninguna salida, o si la salida es distinta de la mostrada arriba, el flujo de bits en uso no es el correcto.
Reproducción de un archivo de audio
Copie un archivo de audio al sistema de archivos de Xillinux. En otras palabras, el archivo de audio debe estar accesible para los comandos dentro del sistema Linux.
El archivo debe estar en formato WAV: PCM sin comprimir, 2 canales, s16le (casi siempre es el formato de los archivos WAV). La frecuencia de muestreo debería ser 48000 Hz, pero 44100 Hz también funcionará razonablemente bien.
Se puede descargar un archivo de audio adecuado desde este enlace.
Hay varios métodos para copiar un archivo a un sistema Linux. Por ejemplo, se puede usar la red Ethernet para copiar el archivo desde otro ordenador al directorio personal de Xillinux con este comando:
$ scp sample.wav root@192.168.1.10:~/
Esto funciona tanto con el símbolo del sistema de Microsoft Windows como con un shell de Linux. Cambie la dirección IP (192.168.1.10 en este ejemplo) por la dirección IP de la placa.
También hay otros métodos para copiar un archivo a Xillinux, como usar NFS o CIFS.
Una vez copiado el archivo al sistema de archivos de Xillinux, use este comando para reproducir el audio:
# cat sample.wav > /dev/xillybus_write_32
Sustituya «sample.wav» por el nombre del archivo que quiera reproducir. El comando que se muestra funciona si el archivo está en el directorio actual.
El comando reproduce el archivo en los auriculares hasta que aparece un nuevo indicador de shell. Debería poder oír la música por un canal (o por ambos, si ha conectado tanto J6/1 como J6/2 a partes separadas del conector de los auriculares).
Es posible detener este comando a mitad de camino con CTRL-C.
Eso es todo. El resto de esta página explica cómo funciona.
Cómo llegan los datos de audio a la FPGA
El comando «cat» copia el contenido del archivo de audio (sample.wav) en un archivo de dispositivo llamado «xillybus_write_32». En los sistemas Linux, este es el método habitual para enviar datos a un controlador de hardware. En este ejemplo, el controlador interactúa con el núcleo IP (IP core) de Xillybus. Como resultado, los datos se envían a un FIFO dentro de la lógica de la FPGA.
Veamos la parte correspondiente del código Verilog mostrado arriba:
fifo_32x512 fifo_32
(
.clk(bus_clk),
// Interface with Xillybus IP core
.srst(!user_w_write_32_open),
.din(user_w_write_32_data),
.wr_en(user_w_write_32_wren),
.full(user_w_write_32_full),
// Interface with application logic
.rd_en(fifo_rd_en),
.dout(fifo_out),
.empty(fifo_empty)
);
Esta es una instanciación (instantiation) de un FIFO estándar. Para una explicación general sobre cómo funciona un FIFO, consulte esta página.
Este FIFO tiene tres puertos relacionados con la inserción de datos en el FIFO: din, wr_en y full (lleno). Los tres puertos están conectados al núcleo IP de Xillybus. En otras palabras, las tres señales (user_w_write_32_data, user_w_write_32_wren y user_w_write_32_full) están conectadas a un módulo que se llama xillybus. Esta disposición permite que el núcleo IP de Xillybus escriba datos en el FIFO.
Xillybus usa esta disposición para llenar el FIFO con los datos que el software escribe en /dev/xillybus_write_32. Xillybus intenta continuamente escribir la mayor cantidad de datos posible en el FIFO, pero nunca causa un desbordamiento (overflow) (es decir, obedece la señal full (lleno) del FIFO).
En resumen, esto es lo que ocurre:
- El comando «cat» copia los datos de sample.wav al archivo de dispositivo (/dev/xillybus_write_32).
- El driver de Xillybus copia esos datos en un búfer DMA.
- La lógica de Xillybus dentro de la FPGA (el núcleo IP de Xillybus) lee los datos del búfer DMA y los escribe en el FIFO.
- La lógica de aplicación dentro de la FPGA lee los datos del FIFO y los consume.
Todas estas operaciones tienen lugar simultánea y continuamente.
Para más información sobre Xillybus, consulte esta serie de páginas, en particular esta página.
Cómo se genera la señal de audio
Hasta ahora se ha explicado cómo llegan los datos a la lógica de aplicación dentro de la FPGA. Veamos ahora cómo se convierten esos datos en audio.
Primero, preste atención a esta línea del código Verilog:
assign J6 = { pwm_right, pwm_left };
De acuerdo con esto, las dos salidas de audio consisten en pwm_right y pwm_left. A estos dos registros se les asignan valores de la siguiente manera:
always @(posedge bus_clk)
begin
pwm_level <= pwm_level + 1;
[ ... ]
pwm_left <= (threshold_left > pwm_level);
pwm_right <= (threshold_right > pwm_level);
[ ... ]
end
Nótese que pwm_level es un contador simple. Este registro tiene 11 bits, de modo que cuenta de 0 a 2047 y luego vuelve a empezar desde 0.
El valor de pwm_left es «1» cuando threshold_left es mayor que pwm_level. En otras palabras, threshold_left se compara con un contador que recorre repetidamente todos los números de 0 a 2047. Cuanto mayor sea el valor de threshold_left, más tiempo estará pwm_left en «1». Este es el principio de la modulación por ancho de pulso (PWM): la duración del pulso es linealmente proporcional al valor de la señal analógica que se quiere generar.
pwm_right funciona del mismo modo en relación con threshold_right.
threshold_left y threshold_right contienen los datos del archivo WAV que se envían a través del núcleo IP de Xillybus. Veamos en detalle cómo ocurre esto.
Primero, observemos la parte de la instanciación del FIFO que está relacionada con la lectura del FIFO:
// Interface with application logic
.rd_en(fifo_rd_en),
.dout(fifo_out),
.empty(fifo_empty)
fifo_rd_en se define de la siguiente manera:
wire fifo_rd_en = !fifo_out_valid && !fifo_empty;
Por tanto, la habilitación de lectura del FIFO está en alto cuando el FIFO no está vacío y cuando fifo_out_valid está en bajo. Veamos ahora la definición de fifo_out_valid:
always @(posedge bus_clk)
begin
[ ... ]
if (fifo_rd_en)
fifo_out_valid <= 1;
else if (next_word)
fifo_out_valid <= 0;
end
El significado de fifo_out_valid es que este registro está en alto cuando la salida del FIFO es válida. Más concretamente, fifo_out_valid está en alto cuando la salida del FIFO aún no se ha consumido. Por eso este registro pasa a alto un ciclo de reloj después de que fifo_rd_en esté en alto. Este registro pasa a bajo cuando next_word está en alto. Como veremos más abajo, la lógica que implementa la PWM consume la salida del FIFO cuando next_word está en alto.
next_word se define de la siguiente manera:
wire next_word = (pwm_level == 11'h7ff);
Recuerde que pwm_level es un contador que recorre todos los valores entre 0 y 2047. La codificación hexadecimal de 2047 es 7ff. Por tanto, next_word está en alto justo antes de que pwm_level vaya a volver a cero.
¿Con qué frecuencia está next_word en alto? La frecuencia de bus_clk es 100 MHz. next_word está en alto una vez por cada ronda de 2048 ciclos de reloj. 100 MHz ÷ 2048 ≈ 48828 Hz. Por tanto, next_word está en alto aproximadamente 48828 veces por segundo.
Antes mencioné que next_word está en alto cuando se consume la salida del FIFO. Esta es la parte correspondiente del código Verilog:
always @(posedge bus_clk)
begin
[ ... ]
if (next_word && fifo_out_valid)
begin
// The audio samples are signed integers. Change them to
// unsigned by adding 1024.
threshold_left <= fifo_out[15:5] + 1024;
threshold_right <= fifo_out[31:21] + 1024;
end
else if (next_word) // FIFO's output not valid, keep silent
begin
threshold_left <= 0;
threshold_right <= 0;
end
[ ... ]
end
Observemos primero que, cuando next_word está en alto, se asignan nuevos valores a threshold_left y threshold_right. Si fifo_out_valid está en bajo, los valores de estos dos registros se vuelven cero. Esto ocurre cuando no se envían datos al FIFO, de modo que este se vacía.
Si fifo_out_valid está en alto, significa que el puerto dout del FIFO contiene el valor de una muestra de audio. Ese valor representa la señal analógica de los dos canales estéreo. Cada muestra contiene dos números con signo representados en complemento a dos de 16 bits.
La muestra de audio que pertenece al canal estéreo izquierdo se encuentra en fifo_out[15:0]. Es un número con signo entre -32768 y 32767. Se eliminan los cinco bits menos significativos, de modo que el rango de fifo_out[15:5] está entre -1024 y 1023. Por tanto, la expresión «fifo_out[15:5] + 1024» es un número sin signo entre 0 y 2047. Este rango de números es adecuado para compararlo con pwm_level.
En consecuencia, cuando fifo_out[15:0] es igual a -32768, a threshold_left se le asigna el valor cero. La condición «threshold_left > pwm_level» no se cumple nunca, así que pwm_left permanece en bajo todo el tiempo. En cambio, cuando fifo_out[15:0] es igual a 32767, el valor de threshold_left es 2047. Como resultado, pwm_left está en alto casi todo el tiempo. Así es como fifo_out[15:0] controla el tiempo que pwm_left permanece en alto en cada pulso. fifo_out[31:16] controla pwm_right de la misma manera.
Para resumir todo el mecanismo: next_word está en alto una vez cada 2048 ciclos de reloj. Cuando next_word está en alto, la salida del FIFO se ajusta y se copia en threshold_left y threshold_right. Esto consume la salida del FIFO, así que fifo_out_valid pasa a bajo. En consecuencia, si el FIFO no está vacío, fifo_rd_en pasa a alto para leer una nueva muestra de audio del FIFO.
Recuerde que el núcleo IP de Xillybus llena este FIFO con el contenido de sample.wav. Por tanto, hay un flujo de muestras de audio desde el contenido de sample.wav hasta threshold_left y threshold_right. Como ya se ha dicho, next_word está en alto aproximadamente 48828 veces por segundo. Esa es la frecuencia de muestreo de este mecanismo.
threshold_left controla la proporción de tiempo que pwm_left está en alto. Lo mismo ocurre con threshold_right y pwm_right. Por último, pwm_right y pwm_left están conectados al puerto de salida llamado J6, de modo que estas son las señales que se ven en el conector de pines.
Nótese que cuando next_word está en alto ocurren dos cosas: se consume una muestra de audio y pwm_level empieza a contar desde cero. Por tanto, se genera un pulso por cada muestra de audio.
Impresión de «PWM»
Antes le recomendé que usara el comando «head /dev/xillybus_read_32» para asegurarse de que la FPGA contiene el flujo de bits (bitstream) correcto. El resultado esperado era que «PWM» se imprimiera muchas veces. Esto está implementado por esta parte del código Verilog:
// Send the text "PWM" to reassure that the correct bitstream is used.
assign user_r_read_32_eof = 0;
assign user_r_read_32_empty = 0;
assign user_r_read_32_data = 32'h0a_4d_57_50; // "PWM" + LF
Si observa xillydemo.v antes de hacer los cambios, verá que user_r_read_32_rden, user_r_read_32_data y user_r_read_32_empty estaban conectados al FIFO. El núcleo IP de Xillybus usa estas señales para leer datos de un FIFO y ponerlos a disposición como un flujo de datos que se presenta con /dev/xillybus_read_32.
Antes del cambio en xillydemo.v, estas señales estaban conectadas al mismo FIFO al que el núcleo IP de Xillybus escribe. El resultado era un bucle de retorno (loopback): los datos que el software escribía en /dev/xillybus_write_32 los insertaba primero el núcleo IP en el FIFO. Después, el núcleo IP de Xillybus leía los datos del FIFO y los presentaba con /dev/xillybus_read_32. La finalidad de este bucle de retorno es servir de punto de partida para aprender cómo funciona Xillybus.
Tras el cambio en xillydemo.v, estas señales quedan desconectadas del FIFO. En su lugar, user_r_read_32_data siempre vale 0x0a4d5750 y user_r_read_32_empty siempre es cero. Además, la lógica ignora user_r_read_32_rden. Esto crea un FIFO imaginario que nunca está vacío. La salida de este FIFO imaginario siempre tiene el mismo valor: 0x0a4d5750. El núcleo IP de Xillybus se comporta como si hubiera un FIFO lleno permanentemente con ese valor constante. Por tanto, al leer de /dev/xillybus_read_32, la palabra 0x0a4d5750 llega repetidamente. Cuando se imprime esta palabra, se interpreta como cuatro bytes: 0x50, 0x57, 0x4d y 0x0a. En otras palabras, los caracteres P, W, M y un salto de línea (que en Linux se usa para marcar el final de una línea).
La relación entre el código Verilog y los pines reales
El código Verilog anterior conecta las señales PWM a J6, pero, ¿cómo llega esto al conector de pines? La respuesta se encuentra en xillydemo.xdc. Este archivo forma parte del proyecto Vivado que crea el flujo de bits (bitstream) (en el directorio «vivado-essentials»).
xillydemo.xdc contiene diversa información necesaria para que la FPGA funcione correctamente como componente electrónico. Entre otras cosas, este archivo contiene estas líneas:
[ ... ]
## J6 on board (BANK33 VADJ)
set_property PACKAGE_PIN U22 [get_ports {J6[0]}]; #J6/1 = IO_B33_LN2
set_property PACKAGE_PIN T22 [get_ports {J6[1]}]; #J6/2 = IO_B33_LP2
set_property PACKAGE_PIN W22 [get_ports {J6[2]}]; #J6/3 = IO_B33_LN3
set_property PACKAGE_PIN V22 [get_ports {J6[3]}]; #J6/4 = IO_B33_LP3
set_property PACKAGE_PIN Y21 [get_ports {J6[4]}]; #J6/5 = IO_B33_LN9
set_property PACKAGE_PIN Y20 [get_ports {J6[5]}]; #J6/6 = IO_B33_LP9
set_property PACKAGE_PIN AB22 [get_ports {J6[6]}]; #J6/7 = IO_B33_LN7
set_property PACKAGE_PIN AA22 [get_ports {J6[7]}]; #J6/8 = IO_B33_LP7
[ ... ]
La primera línea dice que la señal J6[0] debe conectarse a U22. Esta es una posición en el encapsulado físico de la FPGA. Según los esquemas de Smart Zynq, este pin de la FPGA está conectado al primer pin del conector de pines. Las posiciones de los demás puertos se definen de la misma manera.
Antes mencioné que cualquier pin del rango de J6/3 a J6/34 se puede usar como tierra, porque esos pines de salida tienen el valor «0». Esto es cierto porque J6 está formado por 34 bits, de acuerdo con esta línea al comienzo de xillydemo.v:
inout [33:0] J6, //BANK33 VADJ
Recuerde que la asignación de valor de J6 es la siguiente:
assign J6 = { pwm_right, pwm_left };
Esto significa que J6[0] es igual a pwm_left y J6[1] es igual a pwm_right. ¿Y el resto? De acuerdo con la sintaxis de Verilog, a todos los demás bits se les asigna el valor cero.
El sesgo de corriente continua (DC bias)
El conector de pines está conectado a las salidas lógicas de la FPGA. Cada uno de estos pines tiene una tensión de aproximadamente 3.3V cuando el estado lógico es «1». Cuando el estado lógico es «0», la tensión es de aproximadamente 0V.
Si el valor de la muestra de audio original es cero, los valores de threshold_left y threshold_right serán 1024. En otras palabras, pwm_right y pwm_left estarán en alto durante la mitad del tiempo, en promedio. La tensión media de corriente continua (DC) es, por tanto, 3.3V ÷ 2 = 1.65V. Así pues, aunque las muestras de audio del archivo WAV tengan un balance de continua perfecto, los auriculares quedan sometidos a una componente de continua de 1.65V.
Por tanto, la finalidad de la resistencia de 100 Ω no es solo reducir el nivel de sonido, sino limitar la corriente continua. Pero incluso sin esta resistencia, la corriente eléctrica probablemente será inofensiva debido al límite propio de la FPGA y a la resistencia eléctrica de los auriculares. La resistencia es simplemente una medida de precaución.
Resumen
Este proyecto ha mostrado cómo usar un pin de salida digital para producir una señal de audio analógica que se puede conectar directamente a unos auriculares. El énfasis de este proyecto ha estado en presentar el uso de un flujo Xillybus para enviar datos desde el software a la FPGA. También se ha mostrado una implementación sencilla de la modulación por ancho de pulso (PWM).
