01signal.com

将普通耳机连接到 digital output pin 并听音乐

该网页属于一组探索 Smart Zynq board功能的小项目

介绍

本教程介绍如何将普通耳机连接到 Smart Zynq 板并听音乐。该项目的目的是演示如何使用 Xillybus stream 向 FPGA发送连续数据。此处还显示了实现 PWM modulator 的 Verilog 代码。

此处显示的代码不是如何实现音频输出的示例。实现模拟输出的常用方法是一种更复杂的技术,称为 Sigma-Delta。该技术可以在 FPGA上实现,但理论背景更难以理解。

此实现的另一个缺点是它的 sample rate 不准确(48828 Hz 而不是 48000 Hz)。通过更改 logic使用的 clock 的频率可以轻松解决此问题。此处未显示为此目的操作 clocks 的主题,因为此示例侧重于简单性而不是准确性。

本次演示的设备是:

准备 Vivado 项目

从 demo bundle的 zip 文件( boot partition kit)创建一个新的 Vivado 项目。在文本编辑器中打开 verilog/src/xillydemo.v 。删除标记为“PART 2”的代码部分。相反,插入以下代码片段:

/*
    * 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

或者,从此处下载 xillydemo.v 文件。

按照与为 demo bundle创建 bitstream 文件相同的方式从更新的项目创建 bitstream 文件。同样以相同的方式将 bitstream 文件复制到 TF card (用此项目创建的文件覆盖旧的 xillydemo.bit 文件)。

连接耳机

将 50Ω-200Ω resistor 连接到承载音频信号的 I/O pin : J6/1 (左耳)或 J6/2 (右耳)。为了找到 J6,请在 Smart Zynq 板背面查找写有“Bank 33 VCCIO Vadj”的位置。靠近此标记的 pins 行是我们将使用的 pin header 。因此, J6/1 是最接近 HDMI 连接器的 pin 。

将 resistor 的另一端连接到耳机插头的尖端。鳄鱼夹可用于此目的。

将耳机插头的套筒部分连接到 Smart Zynq的 ground上。 pin header的 ground 定位在 J6/35 或 J6/36。但不建议使用这些 pins,因为它们接近 power supply pins。

相反,也可以使用从 J6/3 到 J6/34 范围内的任何 pin 。 FPGA 将它们视为 output pins,并将它们维持在 '0' logic level。因此,可以将这些 pins 用作 ground。

还可以通过将鳄鱼夹连接到板连接器之一的外部金属部分来获得与 ground 的连接: Ethernet 连接器、 HDMI 连接器或 USB 连接器之一。

启动板子

照常打开 Smart Zynq 电源(或执行 reboot)。下一步是确保正确的 bitstream 文件加载到 FPGA (PL 部分)中。

在 shell prompt处键入命令“head /dev/xillybus_read_32”。此命令从 /dev/xillybus_read_32 读取第一行并打印出结果:

# head /dev/xillybus_read_32
PWM
PWM
PWM
PWM
PWM
PWM
PWM
PWM
PWM
PWM

如果此命令没有输出,或者输出与上面显示的不同,则说明使用了不正确的 bitstream 。

播放音频文件

将音频文件复制到 Xillinux的 file system。换句话说,音频文件应该可用于 Linux 系统内部的命令。

该文件应为 WAV 格式: Uncompressed PCM, 2 channels, s16le (这几乎总是 WAV 文件的格式)。 sampling rate 应该是 48000 Hz,但 44100 Hz 也能很好地工作。

可以从此链接下载足够的音频文件。

有多种方法可以将文件复制到 Linux 系统。例如,可以使用 Ethernet network 通过以下命令将文件从另一台计算机复制到 Xillinux的 home directory :

$ scp sample.wav root@192.168.1.10:~/

这适用于 Microsoft Windows、 command prompt 以及 Linux shell。将 IP address (本例中为192.168.1.10 )更改为主板的 IP address。

还有其他方法可以将文件复制到 Xillinux 。例如,使用 NFS 或 CIFS。

将文件复制到 Xillinux的 file system后,使用以下命令播放音频:

# cat sample.wav > /dev/xillybus_write_32

将“sample.wav”替换为您要播放的文件的名称。如果文件位于 current directory中,则此处显示的命令有效。

该命令在耳机上播放文件,直到出现新的 shell prompt 。您应该能够用一只耳朵(或两只耳朵,如果您将 J6/1 和 J6/2 连接到耳机插头的不同部分)听到音乐。

可以使用 CTRL-C中途停止此命令。

就是这样。本页的其余部分解释了其工作原理。

音频数据如何到达 FPGA

“cat”命令将音频文件 (sample.wav) 的内容复制到名为“xillybus_write_32”的 device file 中。在 Linux 系统中,这是向 hardware driver发送数据的常用方法。在此示例中, driver 与 Xillybus的 IP core连接。结果,数据被发送到 FPGA的 logic内部的 FIFO 。

让我们看看上面给出的 Verilog 代码中的相关部分:

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)
      );

这是标准 FIFO的 instantiation 。有关 FIFO 工作原理的一般说明,请参阅此页面

这个 FIFO 有3个与向 FIFO插入数据相关的 ports : din、 wr_en 和 full。所有这三个 ports 都连接到 Xillybus IP core。换句话说,三个信号(user_w_write_32_data、 user_w_write_32_wren 和 user_w_write_32_full)连接到名为 xillybus的 module 。这种布置允许 Xillybus IP core 将数据写入 FIFO。

Xillybus 使用这种安排将软件写入 /dev/xillybus_write_32的数据填充到 FIFO 。 Xillybus 不断尝试将尽可能多的数据写入 FIFO,但它永远不会导致 overflow (即它服从 FIFO的 full 信号)。

总而言之,发生的情况是这样的:

Simplified data flow diagram for data playback with Xillybus

所有这些操作都是同时连续进行的。

有关 Xillybus的更多信息,请参阅本系列页面,特别是本页面

音频信号是如何创建的

到目前为止的描述解释了数据如何到达 FPGA内部的 application logic 。现在我们将看看数据如何转换为音频。

首先,注意 Verilog 代码中的这一行:

assign J6 = { pwm_right, pwm_left };

据此,两个音频输出由 pwm_right 和 pwm_left组成。这两个 registers 的赋值如下:

always @(posedge bus_clk)
     begin
	pwm_level <= pwm_level + 1;

 [ ... ]
	pwm_left <= (threshold_left > pwm_level);
	pwm_right <= (threshold_right > pwm_level);
 [ ... ]
    end

请注意, pwm_level 是一个简单的 counter。这个 register 由11位组成,所以它从0计数到2047,然后又从0开始。

当 threshold_left 大于 pwm_level时, pwm_left 的值为 '1' 。换句话说, threshold_left 与重复遍历从 0 到 2047 的所有数字的 counter 进行比较。 threshold_left 的值越高, pwm_left 具有值 '1'的时间就越长。这就是 PWM的原理: pulse的长度与我们想要生成的模拟信号的值成线性比例。

pwm_right 的工作方式与 threshold_right相同。

threshold_left 和 threshold_right 包含通过 Xillybus IP core发送的 WAV 文件中的数据。我们现在将详细了解这是如何发生的。

首先我们看一下 FIFO的 instantiation 中与从 FIFO读取相关的部分:

// Interface with application logic
      .rd_en(fifo_rd_en),
      .dout(fifo_out),
      .empty(fifo_empty)

fifo_rd_en 定义如下:

wire 	fifo_rd_en = !fifo_out_valid && !fifo_empty;

因此,当 FIFO 不为空且 fifo_out_valid 为低电平时, FIFO的 read enable 为高电平。那么我们看一下 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

fifo_out_valid 的意义是当 FIFO 输出有效时,这个 register 为高电平。更准确地说,当 FIFO的输出尚未消耗完时, fifo_out_valid 为高电平。这就是为什么这个 register 在 fifo_rd_en 为高电平之后又变成高电平的一个 clock cycle 。当 next_word 为高电平时, register 变为低电平。正如我们将在下面看到的,当 next_word 为高电平时,实现 PWM 的 logic 会消耗 FIFO的输出。

next_word 定义如下:

wire 	next_word = (pwm_level == 11'h7ff);

回想一下, pwm_level 是遍历 0 到 2047 之间所有值的 counter 。2047 的十六进制编码是 7ff。因此,在 pwm_level 即将回到零之前, next_word 处于高电平。

next_word 多久出现一次高电平? bus_clk 的频率是 100 MHz。 2048 clock cycles每轮 next_word 为高一次。 100 MHz ÷ 2048 ≈ 48828 Hz。所以 next_word 每秒大约48828次。

我之前提到过,当 FIFO的输出被消耗时, next_word 为高电平。这是 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

我们首先观察到,当 next_word 为高电平时,新值被分配给 threshold_left 和 threshold_right。如果 fifo_out_valid 为低电平,则这两个 registers 的值变为零。当没有数据发送到 FIFO时会发生这种情况,因此它变为空。

如果 fifo_out_valid 为高,则意味着 FIFO的 dout port 包含 audio sample的值。该值代表两个立体声通道的模拟信号。每个这样的 sample 包含两个以 16-bit 2's complement 格式给出的有符号数字。

fifo_out[15:0]中给出了属于左立体声通道的 audio sample 。这是一个介于 -32768 和 32767 之间的有符号数。删除了低 5 位,因此 fifo_out[15:5] 的范围介于 -1024 和 1023 之间。因此,表达式“fifo_out[15:5] + 1024”是介于 0 和 2047 之间的无符号数。这个数字范围是适合与 pwm_level对比。

因此,当 fifo_out[15:0] 等于 -32768 时, threshold_left 将被赋值为零。条件“threshold_left > pwm_level”永远不会满足,因此 pwm_left 始终保持低电平。另一方面,当 fifo_out[15:0] 等于32767时, threshold_left的值为2047。因此, pwm_left 几乎一直为高。这就是 fifo_out[15:0] 控制每个 pulse上 pwm_left 为高电平的时间长度的方式。 fifo_out[31:16] 以同样的方式控制 pwm_right 。

总结一下整个机制: next_word 每 2047 个 clock cycles就会出现一次高电平。当 next_word 为高电平时, FIFO 的输出被调整并复制到 threshold_left 和 threshold_right中。这会消耗 FIFO的输出,因此 fifo_out_valid 变低。因此,如果 FIFO 不为空,则 fifo_rd_en 变高,以便从 FIFO读取新的 audio sample 。

回想一下, Xillybus IP core 用 sample.wav的内容填充了这个 FIFO 。于是就有了一条从 sample.wav 的内容到 threshold_left 、 threshold_right的 audio samples 的数据流。如上所述, next_word 每秒高约 48828 次。这就是这个机制的 sample rate 。

threshold_left 控制 pwm_left 为高电平的时间比例。 threshold_right 和 pwm_right也是如此。最后, pwm_right 和 pwm_left 连接到名为 J6的 output port ,因此这些是 pin header上可见的信号。

请注意,当 next_word 为高电平时,会发生两件事: audio sample 被消耗, pwm_level 从零开始计数。因此,每个 audio sample都会生成一个 pulse 。

打印出“PWM”

早些时候,我鼓励您使用命令“head /dev/xillybus_read_32”,以确保 FPGA 包含正确的 bitstream。预期的结果是“PWM”被打印了很多次。这是 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

如果您像进行更改之前一样查看 xillydemo.v ,您将看到 user_r_read_32_rden、 user_r_read_32_data 和 user_r_read_32_empty 已连接到 FIFO。 Xillybus IP core 使用这些信号从 FIFO 读取数据,并使这些数据作为 /dev/xillybus_read_32提供的数据流提供。

在 xillydemo.v发生变化之前,这些信号连接到 Xillybus IP core 写入的同一个 FIFO 。结果是 loopback: 软件写入 /dev/xillybus_write_32 的数据首先由 Xillybus IP core插入 FIFO 。然后, Xillybus IP core 从 FIFO 读取数据并将其呈现给 /dev/xillybus_read_32。 loopback 的目的是作为学习 Xillybus 工作原理的起点。

xillydemo.v变更后,这些信号与 FIFO断开。相反, user_r_read_32_data 始终等于 0x0a4d5750 ,而 user_r_read_32_empty 始终为零。此外, user_r_read_32_rden 被 logic忽略。这将创建一个虚构的 FIFO ,它永远不为空。这个假想的 FIFO 的输出始终具有相同的值: 0x0a4d5750。 Xillybus IP core 的行为就好像有一个 FIFO 始终填充此常量值。因此,当从 /dev/xillybus_read_32读取时,字 0x0a4d5750 会重复到达。当这个字被打印出来时,它被解释为四个字节: 0x50、 0x57、 0x4d 和 0x0a。换句话说,字符 P、 W、 M 和 line feed (用于标记 Linux中行的结尾)。

Verilog 代码与真实 pins的关系

上面的 Verilog 代码将 PWM 信号连接到 J6,但是这是如何到达 pin header的呢?答案可以在 xillydemo.xdc中找到。该文件是创建 bitstream 的 Vivado 项目的一部分(位于“vivado-essentials”目录中)。

xillydemo.xdc 包含 FPGA 作为电子元件正常工作所需的各种信息。其中,该文件包含以下行:

[ ... ]

## 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

[ ... ]

第一行表示信号 J6[0] 应连接到 U22。这是 FPGA物理包装上的位置。根据 Smart Zynq的 schematics,这个 FPGA pin 连接到 pin header的第一个 pin 。另一个 ports 的位置也以同样的方式定义。

我上面提到,从 J6/3 到 J6/34 范围内的任何 pin 都可以用作 ground,因为这些 output pins 的值为 '0'。这是正确的,因为 J6 由 34 位组成,根据 xillydemo.v开头的这一行:

inout [33:0] J6,  //BANK33 VADJ

回想一下 J6 的赋值如下:

assign J6 = { pwm_right, pwm_left };

这意味着 J6[0] 等于 pwm_left , J6[1] 等于 pwm_right。剩下的呢?根据 Verilog的语法,所有其他位均分配为零值。

DC bias

pin header 连接到 FPGA的 logic outputs。当 logic state 是 '1'时,这些 pins 中的每一个的电压都在 3.3V 左右。当 logic state 为 '0'时,电压在 0V左右。

如果原来的 audio sample 的值为0,则 threshold_left 和 threshold_right 的值为1024。换句话说,平均而言, pwm_right 和 pwm_left 将在一半的时间内处于高电平。因此平均电压 (DC) 为 3.3V ÷ 2 = 1.65V。所以即使 WAV 文件中的 audio samples 有一个完美的 DC balance,耳机暴露给 1.65V 作为 DC 组件。

因此, 100Ω resistor 的目的不仅是降低声级,而且是限制 DC current。但即使没有这个电阻,由于 FPGA自身的限制和耳机的电阻,电流也可能是无害的。 resistor 只是一种预防措施。

概括

该项目展示了如何使用数字 output pin 来生成可直接连接到耳机的模拟音频信号。该项目的重点是展示如何使用 Xillybus stream 将数据从软件发送到 FPGA。还展示了 PWM 的简单实现。

此页面由英文自动翻译。 如果有不清楚的地方,请参考原始页面
Copyright © 2021-2024. All rights reserved. (6f913017)