Xillinux 章节十五 将普通耳机连接到 digital output pin 并播放音乐

本文根据 Xillinux (xillybus)官方的子网站 www.01signal.com 中的内容转载整理而来,如对原始内容感兴趣的可访问下列链接进行查看 01signal:将普通耳机连接到 digital output pin 并听音乐

也感谢xillybus 官方对原始内容的梳理, 本文仅在上述内容中对文中部分翻译内容做了简单微调,以便更好的理解。

介绍

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

Smart Zynq 本身不带音频输出电路,不具有音频的输出能力。本文采用用PWM编码的方式来让digital output pin(数字GPIO口)实现模拟音频的输出功能。(通常音频的输出会使用一种更为复杂的技术(Sigma-Delata)。该技术可以在 FPGA上实现,但理论背景更难理解,本文中所采用的用PWM来模拟输出音频的方式相较而言会更容易实现一些)。

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

本次演示的设备是:

  • Smart Zynq 板(SP,SP2 或 SL)。
  • 一副普通的模拟耳机(题外话:因为该模拟音频输出的方式对耳机有可能造成损坏,所以请不要拿很贵的耳机来做测试,我知道有些程序员的耳机并不便宜)
  • 鳄鱼夹和电线,或者其他可以连接PIN脚的方式。
  • 可选的: 一个 50Ω-200Ω电阻。 添加这个电阻的目的是为了保护耳机和 Smart Zynq 主板免受过大电流的影响。该示例在没有这个电阻的情况下也可以工作,但会存在损坏电子设备的风险。

准备 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 卡(用此项目创建的文件覆盖旧的 xillydemo.bit 文件)。( 创建bitstream的方法,以及复制bitstream部分的操作请参考 Xillinux 章节二 TF卡准备工作之 demo bundle 的使用说明 )

连接耳机

耳机的连接我们需要连接三根信号线,即左耳音频信号,右耳音频信号,以及GND

将 50Ω-200Ω 的电阻连接到承载音频信号的 I/O 管脚PIN上 : J6/1 (左耳)或 J6/2 (右耳)。为了找到 J6,请在 Smart Zynq 板背面查找写有“Bank 33 VCCIO Vadj”的位置。靠近此标记的 排针行是我们将使用的引脚接口 。因此, J6/1 是最接近 HDMI 连接器的引脚 。(详细位置可参考下图)

将电阻的另一端连接到耳机插头的尖端。也可以用鳄鱼夹来进行连接。(备注,如果您的耳机是四段式的耳机接口,GND和MIC的位置有两种标准,互相相反,请自行尝试)

将耳机插头的套筒部分连接到 Smart Zynq的 GND上。 排针的GND定位在 J6/35 或 J6/36。但不建议使用这些 pins,因为它们太接近电源管脚(如使用鳄鱼夹有碰触到电源和地造成短路有硬件损坏的风险)

作为替代,我们也可以使用从 J6/3 到 J6/34 范围内的任何引脚来代替GND 。只需要让FPGA 将它们视为输出管脚,并将它们保持在逻辑’0’电平 (输出低电平)。因此,可以将这些引脚用作GND的功能。

当然也还可以通过将鳄鱼夹连接到板连接器之一的外部金属部分来获得与 ground 的连接: Ethernet 连接器、 HDMI 连接器或 USB 连接器之一(这些接插件的外壳都是接地的)。

PS:如果想自己设计声音转接板的,也可以看本文最后部分,有声音子模块的相关内容,可以直接找PCB 板厂家做板。

启动板子

像往常一样打开 Smart Zynq 电源(或按下POR硬件复位按键进行重启)。下一步是对FPGA(PL)部分加载的bitstream文件(上文中修改并编译的部分)进行验证。

在 shell 命令行中键入命令“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的文件系统中。换句话说,音频文件应适用于 Linux 系统内部的命令。该文件应为 WAV 格式: Uncompressed PCM, 2 channels, s16le (这几乎是 WAV 文件固定的格式)。 采样率应该是 48000 Hz,但 44100 Hz 也能正常工作。

可以从此链接下载测试用的音频文件。

有多种方法可以将文件复制到 Linux 系统。例如,可以使用以太网通过以下命令将文件从另一台计算机复制到 Xillinux的 home 目录 :$ scp sample.wav root@192.168.1.10:~/ 这适用于 Microsoft Windows、 command prompt 以及 Linux shell。将 IP address (本例中为192.168.1.10 )更改为当前网络下主板的 IP 地址。(可以参考 Xillinux 章节十 Windows 通过 SCP 命令 远程传输文件给Xillinux 系统 )

还有其他方法可以将文件复制到 Xillinux 。例如,使用 NFS 或 CIFS。(CIFS服务器方式可以参考 Xillinux 章节十二 在Xillinux 系统上搭建 CIFS服务(samba),实现与Windows 文件共享 )如果是CIFS方式,当前用户目录应该是\root\root\ 备注文件系统中的\root文件 实际在这里应该是\\192.168.1.10\root\root

将文件复制到 Xillinux的 文件系统后使用以下命令播放音频:# cat sample.wav > /dev/xillybus_write_32 (将“sample.wav”替换为您要播放的文件的名称。如果文件位于当前目录中,则此处显示的命令有效)。

cat sample.wav > /dev/xillybus_write_32

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

可以使用 CTRL + C中途停止此命令(音乐播放命令)。

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

音频数据如何到达 FPGA

“cat”命令将音频文件 (sample.wav) 的内容复制到名为“xillybus_write_32”的设备文件中。在 Linux 系统中,这是向 硬件驱动程序发送数据的常用方法。在此示例中, 驱动程序与 Xillybus的 IP 核连接。最终数据被发送到 FPGA逻辑内部的 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 模块的例化 。有关 FIFO 工作原理的说明,请参阅此页面 (页面来自 01signal )

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

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

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

  • “cat”命令将数据从 sample.wav 复制到 device file(设备文件)。 (/dev/xillybus_write_32)。
  • Xillybus的驱动程序将此数据复制到 DMA 缓冲区中。
  • FPGA ( Xillybus IP core)内部的Xillybus的逻辑 从 DMA缓冲区中读取数据并将数据写入 FIFO。
  • FPGA 内部的应用逻辑从 FIFO 读取数据并消耗使用该数据。

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

有关 Xillybus的更多信息,请参阅本系列页面,特别是本页面(页面来自 01signal)

音频信号是如何创建的

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

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

assign J6 = { pwm_right, pwm_left };

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

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 是一个简单的计数器。该寄存器由11位组成,并从0计数到2047,然后又从0开始计数。

当 threshold_left 大于 pwm_level时, pwm_left 的值为 ‘1’ 。换句话说, threshold_left 与反复从 0 到 2047 遍历的计数器 pwm_level 进行比较。 threshold_left 的值越高, pwm_left 具有值 ‘1’的时间就越长。这就是 PWM的原理: 脉冲的长度与我们想要生成的模拟信号的值成线性比例关系。

pwm_right 的工作方式与 threshold_right相同。

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

首先我们看一下 FIFO的 实例 中与从 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的读使能为高电平。那么我们看一下 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 输出有效时,该寄存器为高电平。更准确地说,当 FIFO的输出尚未消耗完时, fifo_out_valid 为高电平。这就是为什么该寄存器在 fifo_rd_en 为高电平后一个时钟周期变为高电平的原因 。当 next_word 为高电平时, 该寄存器(fifo_out_valid)变为低电平。正如我们将在下面看到的,当 next_word 为高电平时,实现 PWM 的逻辑会消耗 FIFO的输出。

next_word 定义如下:

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

回想一下,pwm_level 是一个计数器,它将遍历 0 到 2047 之间的所有值。2047 的十六进制编码是 7ff。因此,在 pwm_level 即将回到零之前的那一刻,next_word 为高电平。

next_word 多久出现一次高电平? bus_clk 的频率是 100 MHz。 每轮 2048 个时钟周期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 为低电平,则这两个寄存器的值变为零。当没有数据发送到 FIFO时会发生这种情况,因此它变为空。

如果 fifo_out_valid 为高,则意味着 FIFO的 dout端口包含 audio sample(音频样本)的值。该值代表两个立体声通道的模拟信号。每个这样的 sample 包含两个以 16-bit 2’s complement (十六位补码表示法)格式给出的有符号数字。

fifo_out[15:0]中给出了属于左立体声通道的音频样本 。这是一个介于 -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] 控制每个脉冲上 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 核用 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的输出端口 ,因此这些是引脚排针上可见的信号。

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

打印出“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核使用这些信号来从 FIFO 读取数据,并将其作为数据流提供给用户,可通过/dev/xillybus_read_32路径访问。

在 xillydemo.v发生变化之前,这些信号连接到 Xillybus IP 核写入的同一个 FIFO 。结果是 loopback(回环): 软件写入 /dev/xillybus_write_32 的数据首先由 Xillybus IP 核插入到 FIFO队列中 。然后, Xillybus IP 核再从 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 被逻辑忽略。这会创建一个虚构的 FIFO ,它永远不为空。这个假想的 FIFO 的输出始终具有相同的值: 0x0a4d5750。 Xillybus IP核的行为就好像有一个始终充满此常量值的FIFO。因此,当从 /dev/xillybus_read_32读取时,字 0x0a4d5750 会重复到达。当这个字被打印出来时,它被解释为四个字节: 0x50、 0x57、 0x4d 和 0x0a。换句话说,字符 P、 W、 M 和 换行符(在 Linux 中用于标记行的结尾)。

Verilog 代码与真实引脚的关系

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

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

[ ... ] 

## 船上 J6 (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的 原理图,这个 FPGA引脚连接到J6排针的第一个引脚上 。其他端口的位置也以同样的方式定义。

我上面提到,从 J6/3 到 J6/34 范围内的任何引脚都可以用作接地,因为这些输出引脚的值为“0”。这是正确的,因为根据 xillydemo.v 开头的这一行,J6 由 34 位组成:

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

回想一下 J6 的赋值如下:

assign J6 = { pwm_right, pwm_left };

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

DC bias(直流偏置电压)

排针连接到 FPGA的逻辑输出。当逻辑状态输出是 ‘1’时,这些引脚中的每一个的电压都在 3.3V 左右。当逻辑状态输出为 ‘0’时,电压在 0V左右。

如果原始音频样本采样的值为0,则 threshold_left 和 threshold_right 的值为1024。换句话说,平均而言, pwm_right 和 pwm_left 将在一半的时间内处于高电平。因此平均电压 (DC) 为 3.3V ÷ 2 = 1.65V。所以即使 WAV 文件中的音频样本具有一个完美的直流平衡( DC balance),耳机也会暴露在 1.65V 的直流分量下。

因此, 外接50Ω-200Ω电阻的目的不仅是降低声级,而且是限制直流电流。但即使没有这个电阻,由于 FPGA自身驱动能力的限制和耳机自身的线圈电阻,电流也可能是无害的。电阻只是一种预防措施。

概括

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

声音子模块

前面介绍的连接方法是用了鳄鱼夹的连接的方式,为了方便演示,这里也简单画了一个声音子模块,整个成本下来不超过2人民币,大家有兴趣的可以自己做板焊接调试(文件里的gerber 压缩包可以直接投板厂做板)。

声音子模块下载地址如下:(板子上有两个0603的电阻,对手工焊接能力有一点要求)

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注