本文根据 Xillinux (xillybus)官方的子网站 www.01signal.com 中的内容转载整理而来,如对原始内容感兴趣的可访问下列链接进行查看 01signal:将普通耳机连接到 digital output pin 并听音乐
也感谢xillybus 官方对原始内容的梳理, 本文仅在上述内容中对文中部分翻译内容做了简单微调,以便更好的理解。
原文内容适配于Smart Zynq SP/SP2/SL的主板的排针接口,经过修改IO定义同样也可以用于Lemon Zynq 的排针接口,又因为Lemon Zynq主板上本身自带了PWM音频输出部分电路,所以为了防止操作上的混淆,特地单独开一个Lemon Zynq 的单独章节作为介绍。(核心操作和内容是一致的)
介绍
本教程介绍如何将普通耳机连接到 Lemon Zynq 主板并听音乐。该项目的目的是演示如何使用 Xillybus stream 向 FPGA发送连续数据。此处还展示了实现 PWM 调试的 Verilog 代码。
本文采用用PWM编码的方式来让digital output pin(数字GPIO口)实现模拟音频的输出功能。(通常音频的输出会使用一种更为复杂的技术(Sigma-Delata)。该技术可以在 FPGA上实现,但理论背景更难理解,本文中所采用的用PWM来模拟输出音频的方式相较而言会更容易实现一些)。
此方法刚好适配Lemon Zynq主板的音频输出电路(带模拟滤波),所以如果您使用的是Lemon Zynq就不需要额外的再去制作音频部分的转接模块了。如果您对如何用普通IO模拟音频感兴趣,可以参看本章节的Smart Zynq 版本。
用此方法实现音频输出的另一个缺点是它的 sample rate(采样率) 不准确(48828 Hz 而不是 48000 Hz)。通过更改 logic使用的 clock 的频率可以轻松解决此问题。此处未显示为此目的操作 clocks 的主题,因为此示例侧重于简单性而不是准确性。
本次演示的设备是:
- Lemon Zynq 的主板(带3.5寸音频输出接口)。
- 一副普通的模拟耳机(题外话:因为该模拟音频输出的方式对耳机有可能造成损坏,所以请不要拿很贵的耳机来做测试,我知道有些程序员的耳机并不便宜)。
准备 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 AUDIO_PWM_L = pwm_left;
assign AUDIO_PWM_R = pwm_right;
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 的使用说明 )
连接耳机
因为我们的Lemon Zynq 主板上已经自带了PWM 音频部分电路(含滤波电路),所以这里我们只需要将耳机插入3.5mm 的音频AUDIO OUT接口上即可。(位子靠近USB HOST接插件,如下图所示)
启动板子
像往常一样打开 Lemon Zynq 电源(或按下POR硬件复位按键进行重启)。下一步是对FPGA(PL)部分加载的bitstream文件(上文中修改并编译的部分)进行验证。
在 shell 命令行中键入命令“head /dev/xillybus_read_32”。
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 。您应该能够通过耳机听到立体声的音乐了。
可以使用 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 AUDIO_PWM_L = pwm_left;
assign AUDIO_PWM_R = pwm_right;
据此,两个音频输出由 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 连接到名为 T17和R18的IO输出口 ,这些IO口将最终连接到主板的音频输出部分电路上。
请注意,当 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 信号连接到 AUDIO_PWM_L,AUDIO_PWM_R两个信号上,但是这是如何到达音频输出部分电路上的呢?答案可以在 xillydemo.xdc中找到。该文件是创建 bitstream 的 Vivado 项目的一部分(位于“vivado-essentials”目录中)。
xillydemo.xdc 包含 FPGA 作为电子元件正常工作所需的各种信息。其中,该文件包含以下行:
[ ... ]
# AUDIO outputs
set_property -dict {PACKAGE_PIN T17 IOSTANDARD LVCMOS33} [get_ports AUDIO_PWM_L]
set_property -dict {PACKAGE_PIN R18 IOSTANDARD LVCMOS33} [get_ports AUDIO_PWM_R]
[ ... ]
第一行表示信号 AUDIO_PWM_L 应连接到 T17。这是 FPGA物理封装上的位置。根据 Lemon Zynq 的 原理图,这个 FPGA引脚连接到音频输出电路的LEFT信号上 。其他端口的位置也以同样的方式定义。
概括
该项目展示了如何使用数字输出引脚来生成可直接连接到耳机的模拟音频信号。该项目的重点是展示如何使用 Xillybus stream 将数据从软件发送到 FPGA。还展示了 PWM 的简单实现。
音频部分的原理图
板载音频输出电路通过一个4阶Sallen-Key Butterworth低通滤波器输出立体声音频。这个滤波器的输入连接到Zynq PL引脚R18与T17脚。当FPGA输出脉宽调制过的数字信号时,音频孔将输出对应的模拟电压。
这部分电路同样也可以被简单的IO 经过电阻限流来替代,详情可以看本章节(Smart Zynq 对应的版本章节内容)Xillinux 章节十五 将普通耳机连接到 digital output pin 并播放音乐 (Smart Zynq SP/SP2/SL) 。
关于PYNQ运行本章节内容的说明
1) PYNQ-Z2 板载的音频部分使用了专用的音频芯片,所以本章节内容不适用于PYNQ-Z2的主板,但是PYNQ-Z2的主板可以查看本章节(Smart ZYNQ对应的版本章节)Xillinux 章节十五 将普通耳机连接到 digital output pin 并播放音乐 (Smart Zynq SP/SP2/SL)来用普通IO口来模拟输出音频信息。
2) PYNQ-Z1虽然电路上和Lemon Zynq 相似, 但是因为PYNQ-Z1是单声道的,又因为PYNQ-Z1的T17是接在AUD_SD(音频使能端),而我们的Lemon ZYNQ T17是接在音频的左声道上。所以如果PYNQ-Z1需要使用本工程,请将AUDIO_PWM_L 对应的值修改为”1″。
assign AUDIO_PWM_L = 1'b1;
assign AUDIO_PWM_R = pwm_right;
当然PYNQ-Z1仍然可以像Z2一样参考 Xillinux 章节十五 将普通耳机连接到 digital output pin 并播放音乐 (Smart Zynq SP/SP2/SL) 来用普通IO口来模拟输出音频信息(这种方法可以让PYNQ-Z1输出立体声音频)。