该网页属于一组探索 Smart Zynq 电路板功能的小项目。
这个项目也在 HelloFPGA上发表,推荐给中国读者。
介绍
本教程介绍如何将普通耳机连接到 Smart Zynq 板并听音乐。该项目的目的是演示如何使用 Xillybus 数据流向 FPGA发送连续数据。此处还显示了实现 PWM modulator 的 Verilog 代码。
此处显示的代码不是如何实现音频输出的示例。实现模拟输出的常用方法是一种更复杂的技术,称为 Sigma-Delta。该技术可以在 FPGA上实现,但理论背景更难以理解。
此实现的另一个缺点是它的采样 rate (sample rate)不准确(48828 Hz 而不是 48000 Hz)。通过更改逻辑使用的时钟的频率可以轻松解决此问题。此处未显示为此目的操作时钟的主题,因为此示例侧重于简单性而不是准确性。
本次演示的设备是:
- Smart Zynq 板(SP 或 SL)。
- 一副普通的模拟耳机。
- 鳄鱼夹和电线,或用于连接到板的引脚的其他方式。
- 选修的: 一个 50Ω-200Ω 电阻。电阻的目的是保护耳机和 Smart Zynq 板免受过大电流的影响。该示例在没有电阻的情况下也可以工作,但存在损坏电子设备的风险。
准备 Vivado 项目
从演示包的(demo bundle)的 zip 文件(启动 partition kit(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 文件。
按照与为演示包的创建比特流(bitstream)文件相同的方式从更新的项目创建比特流文件。同样以相同的方式将比特流文件复制到 TF 卡(用此项目创建的文件覆盖旧的 xillydemo.bit 文件)。
连接耳机
将 50Ω-200Ω 电阻连接到承载音频信号的 I/O 引脚: J6/1 (左耳)或 J6/2 (右耳)。为了找到 J6,请在 Smart Zynq 板背面查找写有“Bank 33 VCCIO Vadj”的位置。靠近此标记的引脚行是我们将使用的排针。因此, J6/1 是最接近 HDMI 连接器的引脚。
将电阻的另一端连接到耳机插头的尖端。鳄鱼夹可用于此目的。
将耳机插头的套筒部分连接到 Smart Zynq的接地上。排针的接地定位在 J6/35 或 J6/36。但不建议使用这些引脚,因为它们接近 power supply 引脚。
相反,也可以使用从 J6/3 到 J6/34 范围内的任何引脚。 FPGA 将它们视为输出引脚(output pins),并将它们维持在 '0' 逻辑级。因此,可以将这些引脚用作接地。
还可以通过将鳄鱼夹连接到板连接器之一的外部金属部分来获得与接地的连接: Ethernet 连接器、 HDMI 连接器或 USB 连接器之一。
启动板子
照常打开 Smart Zynq 电源(或执行重启(reboot))。下一步是确保正确的比特流文件加载到 FPGA (PL 部分)中。
在 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
如果此命令没有输出,或者输出与上面显示的不同,则说明使用了不正确的比特流。
播放音频文件
将音频文件复制到 Xillinux的 file 系统。换句话说,音频文件应该可用于 Linux 系统内部的命令。
该文件应为 WAV 格式: Uncompressed PCM, 2 channels, s16le (这几乎总是 WAV 文件的格式)。采样 rate 应该是 48000 Hz,但 44100 Hz 也能很好地工作。
可以从此链接下载足够的音频文件。
有多种方法可以将文件复制到 Linux 系统。例如,可以使用 Ethernet network 通过以下命令将文件从另一台计算机复制到 Xillinux的 home 目录:
$ scp sample.wav root@192.168.1.10:~/
这适用于微软 Windows(Microsoft Windows)、命令提示符(command prompt)以及 Linux shell。将 IP 地址(本例中为192.168.1.10 )更改为主板的 IP 地址。
还有其他方法可以将文件复制到 Xillinux 。例如,使用 NFS 或 CIFS。
将文件复制到 Xillinux的 file 系统后,使用以下命令播放音频:
# cat sample.wav > /dev/xillybus_write_32
将“sample.wav”替换为您要播放的文件的名称。如果文件位于当前目录(current directory)中,则此处显示的命令有效。
该命令在耳机上播放文件,直到出现新的 shell 提示符。您应该能够用一只耳朵(或两只耳朵,如果您将 J6/1 和 J6/2 连接到耳机插头的不同部分)听到音乐。
可以使用 CTRL-C中途停止此命令。
就是这样。本页的其余部分解释了其工作原理。
音频数据如何到达 FPGA
“cat”命令将音频文件 (sample.wav) 的内容复制到名为“xillybus_write_32”的设备文件(device file)中。在 Linux 系统中,这是向 hardware 驱动程序发送数据的常用方法。在此示例中,驱动程序与 Xillybus的 IP core连接。结果,数据被发送到 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的例化(instantiation)。有关 FIFO 工作原理的一般说明,请参阅此页面。
这 FIFO 有3个与向 FIFO插入数据相关的端口: din、 wr_en 和 full。所有这三个端口都连接到 Xillybus IP core。换句话说,三个信号(user_w_write_32_data、 user_w_write_32_wren 和 user_w_write_32_full)连接到名为 xillybus的模块。这种布置允许 Xillybus IP core 将数据写入 FIFO。
Xillybus 使用这种安排将软件写入 /dev/xillybus_write_32的数据填充到 FIFO 。 Xillybus 不断尝试将尽可能多的数据写入 FIFO,但它永远不会导致溢出(overflow)(即它服从 FIFO的 full 信号)。
总而言之,发生的情况是这样的:
- “cat”命令将数据从 sample.wav 复制到设备文件。 (/dev/xillybus_write_32)。
- Xillybus的驱动程序将此数据复制到 DMA 缓冲区中。
- FPGA ( Xillybus IP core)内部的Xillybus的逻辑从 DMA 缓冲区读取数据并将数据写入 FIFO。
- FPGA 内部的应用逻辑从 FIFO 读取数据并消耗该数据。
所有这些操作都是同时连续进行的。
有关 Xillybus的更多信息,请参阅本系列页面,特别是本页面。
音频信号是如何创建的
到目前为止的描述解释了数据如何到达 FPGA内部的应用逻辑。现在我们将看看数据如何转换为音频。
首先,注意 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 是一个简单的计数器。这个寄存器由11位组成,所以它从0计数到2047,然后又从0开始。
当 threshold_left 大于 pwm_level时, pwm_left 的值为 '1' 。换句话说, threshold_left 与重复遍历从 0 到 2047 的所有数字的计数器进行比较。 threshold_left 的值越高, pwm_left 具有值 '1'的时间就越长。这就是 PWM的原理: 脉冲(pulse)的长度与我们想要生成的模拟信号的值成线性比例。
pwm_right 的工作方式与 threshold_right相同。
threshold_left 和 threshold_right 包含通过 Xillybus IP core发送的 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的读使能(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 输出有效时,这个寄存器为高电平。更准确地说,当 FIFO的输出尚未消耗完时, fifo_out_valid 为高电平。这就是为什么这个寄存器在 fifo_rd_en 为高电平之后又变成高电平的一个时钟周期(clock cycle)。当 next_word 为高电平时,寄存器(register)变为低电平。正如我们将在下面看到的,当 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时钟周期(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 为低电平,则这两个寄存器的值变为零。当没有数据发送到 FIFO时会发生这种情况,因此它变为空。
如果 fifo_out_valid 为高,则意味着 FIFO的 dout 端口包含 audio 采样的值。该值代表两个立体声通道的模拟信号。每个这样的采样(sample)包含两个以 16-bit 2's complement 格式给出的有符号数字。
fifo_out[15:0]中给出了属于左立体声通道的 audio 采样。这是一个介于 -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个时钟周期就会出现一次高电平。当 next_word 为高电平时, FIFO 的输出被调整并复制到 threshold_left 和 threshold_right中。这会消耗 FIFO的输出,因此 fifo_out_valid 变低。因此,如果 FIFO 不为空,则 fifo_rd_en 变高,以便从 FIFO读取新的 audio 采样。
回想一下, Xillybus IP core 用 sample.wav的内容填充了这 FIFO 。于是就有了一条从 sample.wav 的内容到 threshold_left 、 threshold_right的 audio 采样的数据流。如上所述, next_word 每秒高约 48828 次。这就是这个机制的采样 rate 。
threshold_left 控制 pwm_left 为高电平的时间比例。 threshold_right 和 pwm_right也是如此。最后, pwm_right 和 pwm_left 连接到名为 J6的输出端口(output port),因此这些是排针上可见的信号。
请注意,当 next_word 为高电平时,会发生两件事: audio 采样被消耗, pwm_level 从零开始计数。因此,每个 audio 采样都会生成一个脉冲。
打印出“PWM”
早些时候,我鼓励您使用命令“head /dev/xillybus_read_32”,以确保 FPGA 包含正确的比特流。预期的结果是“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。回环的目的是作为学习 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 core 的行为就好像有一 FIFO 始终填充此常量值。因此,当从 /dev/xillybus_read_32读取时,字 0x0a4d5750 会重复到达。当这个字被打印出来时,它被解释为四个字节: 0x50、 0x57、 0x4d 和 0x0a。换句话说,字符 P、 W、 M 和 line feed (用于标记 Linux中行的结尾)。
Verilog 代码与真实引脚的关系
上面的 Verilog 代码将 PWM 信号连接到 J6,但是这是如何到达排针的呢?答案可以在 xillydemo.xdc中找到。该文件是创建比特流的 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 引脚连接到排针的第一个引脚。另一个端口的位置也以同样的方式定义。
我上面提到,从 J6/3 到 J6/34 范围内的任何引脚都可以用作接地,因为这些输出引脚的值为 '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的语法,所有其他位均分配为零值。
直流 bias(DC bias)
排针连接到 FPGA的逻辑输出(logic outputs)。当逻辑状态是 '1'时,这些引脚中的每一个的电压都在 3.3V 左右。当逻辑状态为 '0'时,电压在 0V左右。
如果原来的 audio 采样的值为0,则 threshold_left 和 threshold_right 的值为1024。换句话说,平均而言, pwm_right 和 pwm_left 将在一半的时间内处于高电平。因此平均电压 (直流(DC)) 为 3.3V ÷ 2 = 1.65V。所以即使 WAV 文件中的 audio 采样有一个完美的直流 balance(DC balance),耳机暴露给 1.65V 作为直流组件。
因此, 100Ω 电阻的目的不仅是降低声级,而且是限制直流电流(DC current)。但即使没有这个电阻,由于 FPGA自身的限制和耳机的电阻,电流也可能是无害的。电阻只是一种预防措施。
概括
该项目展示了如何使用数字输出引脚来生成可直接连接到耳机的模拟音频信号。该项目的重点是展示如何使用 Xillybus 数据流将数据从软件发送到 FPGA。还展示了 PWM 的简单实现。