PT8211音频声卡(USB1.1)
PT8211 是一颗 16 位立体声数模转换器(DAC),其作用是将串行的数字音频数据转换为连续的模拟电压。
此程序基于 USB 1.1 协议,外接喇叭后通过 USB 连接上位机可直接作为一个音响播放上位机的音频数据,无需安装任何驱动,同时包含八级音量调档以及数码管显示音量,还有 LED 检测运行状态程序
USB 协议站用的是 WangXuan95 的开源 git链接
这个工程的源码可以参考这个
已经验证过下载烧录即可使用
1、模块来源
此模块的 PCB 版图以及原理图放在百度网盘链接中
百度网盘链接:https://pan.baidu.com/s/1OgGALONO0-4UyK8NWfDljw?pwd=LJPI 提取码: LJPI
实物图如下
2、参数说明
3、USB 原理简介
USB 1.1 协议很复杂,感兴趣的就自行去了解的需要,USB 1.1 协议中文版
3.1、USB 1.1 协议概述
USB(Universal Serial Bus,通用串行总线)采用主从(Host-Device)体系结构:总线上仅存在一个主机(通常为 PC),所有数据传输均由主机发起,设备只能被动响应,不能主动向主机发送数据。
USB 1.1 规范支持两种传输速率:
- 低速(Low Speed):1.5 Mbps;
- 全速(Full Speed):12 Mbps。
本工程采用全速(12 Mbps)模式。在设备描述符中,bcdUSB = 0x0110
3.2、物理层信号
USB 使用一对差分信号线 D+ 与 D- 传输数据。设备通过在其中一根数据线上连接 1.5 kΩ 上拉电阻,向主机表明自身的速率等级:
- 全速设备:D+ 上拉;
- 低速设备:D- 上拉。
本工程为全速设备,故在 D+ 上连接 1.5 kΩ 上拉电阻。该上拉由 FPGA 引脚 usb_dp_pull 控制:当该信号为高电平时,主机检测到 D+ 被上拉,即判定有设备接入,随后启动枚举流程。
3.3、设备枚举
设备接入后,主机通过一系列标准控制请求完成枚举(Enumeration),其基本流程如下:
1. 主机检测到设备接入(D+ 上拉);
2. 主机对总线执行复位;
3. 主机发送 GET_DESCRIPTOR 请求,读取设备描述符;
4. 主机发送 SET_ADDRESS 请求,为设备分配唯一总线地址;
5. 主机读取配置描述符、字符串描述符等;
6. 主机发送 SET_CONFIGURATION 请求,激活配置,设备进入"已配置"状态;
7. 主机根据描述符中声明的设备类,加载相应的驱动程序。
3.4、端点与传输类型
USB 的数据传输以端点(Endpoint)为基本单位。每个端点具有独立的编号与数据方向。
所有设备均须包含端点0(控制端点),用于枚举与配置。
USB 定义了四种传输类型:
| 传输类型 | 特点 | 典型应用 |
|---|---|---|
| 控制传输 | 双向、可靠,用于设备配置 | 枚举、标准请求 |
| 批量传输 | 大数据量、可靠、带重传 | 存储设备、打印机 |
| 中断传输 | 小数据量、周期性、可靠 | 鼠标、键盘 |
| 等时传输 | 固定带宽、周期性、无重传 | 音频、视频 |
音频数据采用等时传输(Isochronous)。等时传输在每个帧内分配固定带宽,以保证数据传输的实时性;由于音频数据具有时效性, 发生错误时不进行重传(重传的数据已失去意义),而是直接丢弃,以保证播放的连续性。
3.5、帧结构
全速模式下,总线以 1 ms 为一帧,由主机周期性广播 SOF(Start of Frame)包标记帧的起始。等时端点按帧分配带宽。
本工程音频数据为 48 kHz 双声道 16 位,即每秒 48000 个采样点,每帧(1 ms)传输 48 个采样点,每帧数据量为:
48 采样点/帧 × 2 声道 × 2 字节 = 192 字节/帧
该值与端点描述符中 wMaxPacketSize = 192(0xC0)一致。
主机以每1.00 ms ±0.0005 ms一次的额定速率发出帧开始(SOF)包。如图中所示,SOF包是由指示包类型的PID和其后的11位的帧号字段构成。
3.6、USB Audio Class 与免驱原理
USB Audio Class(UAC)是 USB 组织(USB-IF)定义的音频设备类规范。符合该规范的设备,在其接口描述符中声明 bInterfaceClass = 0x01(Audio),主机即可加载操作系统内置的音频驱动(如 Windows 的 usbaudio.sys),无需厂商另行提供专用驱动,即所谓"免驱"。
UAC 设备的描述符结构主要包括:
- 音频控制接口(Audio Control Interface):描述设备的音频功能拓扑,包含输入终端、输出终端等;
- 音频流接口(Audio Streaming Interface):描述音频数据的格式与传输端点。
本工程在配置描述符中声明了两个接口:
| 接口 | 接口号 | 类型 | 作用 |
|---|---|---|---|
| 音频控制接口 | 0 | Audio Control | 描述音频拓扑(USB 流输入终端 → 扬声器输出终端) |
| 音频流接口 | 1 | Audio Streaming | 描述数据格式与等时端点(端点 0x01,方向 OUT) |
3.7、PT8211 串行时序
PT8211 与 FPGA 之间采用三线串行接口
| 信号 | 说明 |
|---|---|
| WS | 字选择(Word Select),区分左、右声道 |
| BCK | 位时钟(Bit Clock),每位数据一个时钟 |
| DIN | 数据输入,串行传输,最高位(MSB)在前 |
在串行传输中,每个采样点(双声道)按帧发送:WS 信号的电平区分左、右声道,数据在 BCK 的节拍下由 DIN 逐位送入 DAC。
这三根信号在顶层模块中分别对应 audio_ws、audio_bck、audio_din 三个端口。
4、时序分析
这里代码时序进行简单讲解,如果对 usb 1.1 的时序有兴趣,官方给出的协议里有 PT8211 与 FPGA 之间通过三线串行接口(WS、BCK、DIN)通信,其时序由 LSBJ_audio_driver 模块在音频时钟 audio_clk(3.072 MHz)下产生。
PT8211 靠三根线收数据,分工如下:
| 信号 | 说明 |
|---|---|
| WS | 字选择:拉高 = 左声道,拉低 = 右声道 |
| BCK | 位时钟:FPGA 输出,每跳一下,PT8211 收走一位 |
| DIN | 数据:串行数据,跟着 BCK 一位一位送 |
4.1 初始状态
上电复位后,三根线都是低电平:
4.2 写时序
1. FPGA 把 WS 拉高,告诉 PT8211"接下来写左声道";
2. 之后 BCK 每跳一下,DIN 送一位,共送 16 位左声道数据;
3. 16 位送完,FPGA 把 WS 拉低,告诉 PT8211"接下来写右声道";
4. 再送 16 位右声道数据;
5. 左右各送完一遍,就是一个周期(也就是一帧),然后从头再来。
注:一个周期有96个时钟周期。前48拍送左声道(16位),后48拍送右声道(16位)。其中左右声道中48拍只用了32拍送数据,中间剩下16个空闲拍等待缓冲
4.3 空闲状态
放音时 PT8211 这边没有真正意义的"空闲"——BCK 和 WS 一直在翻转,DIN 一直在送数。
只有当 FIFO 里没数据(比如静音)时,DIN 才会一直是 0,喇叭没声音,但 BCK、WS 照样翻个不停。
5、实战任务
使用逻辑派 FPGA-G1 开发板编写控制 PT8211 运行的模块,包括:
- 音量控制模块
- 顶层文件
- 音频数据并行转串行模块
- 按键消抖模块
- pwm 灯模块(用于展示模块运行状态)
- 异步 FIFO 同步模块
- 时钟树模块(用于分频出各个模块所需的时钟)
6、 程序编写
程序一共分三部分
- IP 核配置(主要是 rPLL 时钟配置)
- USB 1.1 模块程序
- 音频实现程序
由于 USB 1.1 的模块特别复杂而且包含很多协议,这里我们不进行讲解,而且只需要改几个参数就可以适配不同的模块。这里我们不再讲解,大家把他当成一个配置好的 IP 核即可。
如果感兴趣想了解具体实现代码的可以去首页给出的源文件里找,里面有写 USB 1.1 模块的全部代码。这些代码都由开源作者 WangXuan95 开源
`timescale 1ns / 1ns
// Description: LSBJ audio driver
// Author: JerryTech
// License: GPLv3
module LSBJ_audio_driver # (
parameter DEF_LEN = 24, // 每个声道"帧"的位数 = 24
parameter WIDTH = 16, // 真正的音频数据位数 = 16
parameter FIFO_DELAY_MIN = 4 // FIFO 读出来到数据可用,中间隔的时钟数 来对齐延迟
)(
input wire audio_clk, // 音频时钟,3.072MHz
input wire reset,
input wire [WIDTH - 1 :0 ] left_data, //左声道 16 位数据
input wire [WIDTH - 1 :0 ] right_data, //左声道 16 位数据
input wire fifo_read_ready, //FIFO 里有数据可读
output reg fifo_read_en, // 告诉 FIFO"我要读一个数据了"
output reg audio_ws, //字选择(左右声道切换)
output reg audio_bck, //位时钟 → 接 PT8211 的 BCK
output reg audio_din // 串行数据 → 接 PT8211 的 DIN
);
reg [6:0] counter; // 计数器,7 位,0~95 循环
reg [15:0] left_reg; // 左声道数据的"暂存盒"
reg [15:0] right_reg; // 右声道数据的"暂存盒"
//counter计数器 95到0循环
always @ (posedge audio_clk) begin
if(reset) begin
counter <= (2 * 2 * DEF_LEN) - 1;
end
else begin
counter <= counter == 0 ? (2 * 2 * DEF_LEN) - 1 : counter - 6'd1;
end
end
//audio_bck 位时钟
always @ (posedge audio_clk) begin
if(reset) begin
audio_bck <= 0;
end
else begin
audio_bck <= counter[0]; //BCK 就是 audio_clk 除以 2。audio_clk 是 3.072MHz,所以 BCK = 1.536MHz
end
end
//audio_ws 字选择
always @ (posedge audio_clk) begin
if(reset) begin
audio_ws <= 0;
end
else if(counter == 2 * DEF_LEN)begin // counter 到 48 → ws 拉高
audio_ws <= 1;
end
else if(counter == 0) begin // counter 到 0 → ws 拉低
audio_ws <= 0;
end
end
always @ (posedge audio_clk) begin
if(reset) begin
audio_din <= 0;
end
else if((counter <= 2 * WIDTH) && counter > 0)begin // counter 在 1~32
audio_din <= left_reg[((counter - 1) >> 1)]; //每 2 拍输出 1 位,从 left_reg[0] 一路输出到 left_reg[15],16 位数据用 32 拍发完。这是左声道数据
end
else if((counter <= 2 * (WIDTH + DEF_LEN) ) && (counter > 2 * DEF_LEN)) begin //counter 在 49~80
audio_din <= right_reg[(counter - DEF_LEN * 2 - 1) >> 1]; //同样每 2 拍 1 位,发右声道数据
end
else begin
audio_din <= 0; //一圈 96 拍里,前 32 拍发左声道 16 位,中间 16 拍发 0,再 32 拍发右声道 16 位,最后 16 拍发 0。
end
end
//fifo_read_en(向 FIFO 要数据)
always @ (posedge audio_clk) begin
if(reset) begin
fifo_read_en <= 0;
end
else if(counter == 0) begin
fifo_read_en <= 1'b1; //每 96 拍一圈,只有 counter==0 那一拍向 FIFO 读一次。一次读出 32 位
end
else begin
fifo_read_en <= 0;
end
end
always @ (posedge audio_clk) begin
if(reset) begin
left_reg <= 0;
right_reg <= 0;
end
else if(fifo_read_ready) begin
if(counter == (2 * 2 * DEF_LEN) - FIFO_DELAY_MIN - 1) begin // counter == 91
left_reg <= left_data; //把外面送进来的 left_data / right_data 抄进 left_reg / right_reg 暂存盒
right_reg <= right_data;
end
end
else begin
left_reg <= 0;
right_reg <= 0;
end
end
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
`timescale 1ns / 1ns
// Description: LSBJ audio driver
// Author: JerryTech
// License: GPLv3
module LSBJ_audio_driver # (
parameter DEF_LEN = 24, // 每个声道"帧"的位数 = 24
parameter WIDTH = 16, // 真正的音频数据位数 = 16
parameter FIFO_DELAY_MIN = 4 // FIFO 读出来到数据可用,中间隔的时钟数 来对齐延迟
)(
input wire audio_clk, // 音频时钟,3.072MHz
input wire reset,
input wire [WIDTH - 1 :0 ] left_data, //左声道 16 位数据
input wire [WIDTH - 1 :0 ] right_data, //左声道 16 位数据
input wire fifo_read_ready, //FIFO 里有数据可读
output reg fifo_read_en, // 告诉 FIFO"我要读一个数据了"
output reg audio_ws, //字选择(左右声道切换)
output reg audio_bck, //位时钟 → 接 PT8211 的 BCK
output reg audio_din // 串行数据 → 接 PT8211 的 DIN
);
reg [6:0] counter; // 计数器,7 位,0~95 循环
reg [15:0] left_reg; // 左声道数据的"暂存盒"
reg [15:0] right_reg; // 右声道数据的"暂存盒"
//counter计数器 95到0循环
always @ (posedge audio_clk) begin
if(reset) begin
counter <= (2 * 2 * DEF_LEN) - 1;
end
else begin
counter <= counter == 0 ? (2 * 2 * DEF_LEN) - 1 : counter - 6'd1;
end
end
//audio_bck 位时钟
always @ (posedge audio_clk) begin
if(reset) begin
audio_bck <= 0;
end
else begin
audio_bck <= counter[0]; //BCK 就是 audio_clk 除以 2。audio_clk 是 3.072MHz,所以 BCK = 1.536MHz
end
end
//audio_ws 字选择
always @ (posedge audio_clk) begin
if(reset) begin
audio_ws <= 0;
end
else if(counter == 2 * DEF_LEN)begin // counter 到 48 → ws 拉高
audio_ws <= 1;
end
else if(counter == 0) begin // counter 到 0 → ws 拉低
audio_ws <= 0;
end
end
always @ (posedge audio_clk) begin
if(reset) begin
audio_din <= 0;
end
else if((counter <= 2 * WIDTH) && counter > 0)begin // counter 在 1~32
audio_din <= left_reg[((counter - 1) >> 1)]; //每 2 拍输出 1 位,从 left_reg[0] 一路输出到 left_reg[15],16 位数据用 32 拍发完。这是左声道数据
end
else if((counter <= 2 * (WIDTH + DEF_LEN) ) && (counter > 2 * DEF_LEN)) begin //counter 在 49~80
audio_din <= right_reg[(counter - DEF_LEN * 2 - 1) >> 1]; //同样每 2 拍 1 位,发右声道数据
end
else begin
audio_din <= 0; //一圈 96 拍里,前 32 拍发左声道 16 位,中间 16 拍发 0,再 32 拍发右声道 16 位,最后 16 拍发 0。
end
end
//fifo_read_en(向 FIFO 要数据)
always @ (posedge audio_clk) begin
if(reset) begin
fifo_read_en <= 0;
end
else if(counter == 0) begin
fifo_read_en <= 1'b1; //:每 96 拍一圈,只有 counter==0 那一拍向 FIFO 读一次。一次读出 32 位
end
else begin
fifo_read_en <= 0;
end
end
always @ (posedge audio_clk) begin
if(reset) begin
left_reg <= 0;
right_reg <= 0;
end
else if(fifo_read_ready) begin
if(counter == (2 * 2 * DEF_LEN) - FIFO_DELAY_MIN - 1) begin // counter == 91
left_reg <= left_data; //,把外面送进来的 left_data / right_data 抄进 left_reg / right_reg 暂存盒
right_reg <= right_data;
end
end
else begin
left_reg <= 0;
right_reg <= 0;
end
end
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
`timescale 1ns / 1ns
// Description: Async fifo
// Author: JerryTech
// License: GPLv3
// Refer to : https://www.cnblogs.com/liushaowe1/p/16136749.html
// https://blog.csdn.net/qq_38305370/article/details/115056361
// https://www.runoob.com/w3cnote/verilog2-fifo.html
module async_fifo # (
parameter DATA_WIDTH = 32, //数据宽度
parameter ADDR_LEN = 16,
parameter PROGRAM_FULL_SET = 10,
parameter PROGRAM_FULL_CLR = 4,
parameter PROGRAM_EMPTY_SET = 5,
parameter PROGRAM_EMPTY_CLR = 8
)
(
input wire rd_clk,
input wire rd_reset,
input wire wr_clk,
input wire wr_reset,
input wire wr_en,
input wire [DATA_WIDTH - 1:0 ] wr_data,
input wire rd_en,
output reg [DATA_WIDTH - 1:0 ] rd_data,
output wire wr_full,
output wire rd_empty,
output reg wr_full_program,
output reg rd_empty_program
);
localparam ADDR_WIDTH = $clog2(ADDR_LEN); //$clog2(ADDR_LEN) 是"求 ADDR_LEN 需要几位表示"。$clog2(32) = 5,所以 ADDR_WIDTH = 5。地址用 5 位表示 0~31
reg [ADDR_WIDTH :0 ] wr_counter; //写指针、读指针,位宽 [ADDR_WIDTH:0] = 6 位(5 位地址 + 1 位"圈数"标志)
reg [ADDR_WIDTH :0 ] rd_counter;
wire [ADDR_WIDTH :0 ] wr_counter_green; //格雷码版指针
wire [ADDR_WIDTH :0 ] rd_counter_green;
wire [ADDR_WIDTH :0 ] wr_counter_bin; //"同步过来再转回二进制"的指针
wire [ADDR_WIDTH :0 ] rd_counter_bin;
wire [ADDR_WIDTH :0 ] wr_counter_program_cal; //是"算出来的当前货量"
wire [ADDR_WIDTH :0 ] rd_counter_program_cal;
reg [ADDR_WIDTH :0 ] wr_counter_green_sync0; // 是两级同步用的两个触发器(打两拍)
reg [ADDR_WIDTH :0 ] rd_counter_green_sync0;
reg [ADDR_WIDTH :0 ] wr_counter_green_sync1;
reg [ADDR_WIDTH :0 ] rd_counter_green_sync1;
reg [DATA_WIDTH - 1:0 ] ram_block [ADDR_LEN - 1:0 ]; //32 个格子,每格 32 位。reg [31:0] ram_block [31:0] 就是一个 32×32 的内存。
//Write process
always @ (posedge wr_clk) begin
if(wr_reset) begin
wr_counter <= 0;
end
else if(wr_counter_program_cal <= ADDR_LEN - 1 && wr_en) begin //仓库没满(货量 ≤ 31)而且写使能有效
wr_counter <= wr_counter + 1;
ram_block[wr_counter[ADDR_WIDTH - 1:0]] <= wr_data; //把数据写进 ram_block[wr_counter[4:0]](用指针的低 5 位当地址)
end
end
//Read process
always @ (posedge rd_clk) begin //"仓库没空(货量 ≥ 1)而且读使能有效
if(rd_reset) begin
rd_counter <= 0;
end
else if(rd_counter_program_cal >= 1 && rd_en) begin
rd_counter <= rd_counter + 1;
rd_data <= ram_block[rd_counter[ADDR_WIDTH - 1:0]]; //读出 ram_block[rd_counter[4:0]],放到 rd_data。
end
end
//格雷码转换
assign wr_counter_green = wr_counter ^ (wr_counter >>> 1);
assign rd_counter_green = rd_counter ^ (rd_counter >>> 1);
//用格雷码进行两级同步消除亚稳态
always@(posedge wr_clk) begin
if(wr_reset) begin
rd_counter_green_sync0 <= 0;
rd_counter_green_sync1 <= 0;
end
else begin
rd_counter_green_sync0 <= rd_counter_green;
rd_counter_green_sync1 <= rd_counter_green_sync0;
end
end
always@(posedge rd_clk) begin
if(rd_reset) begin
wr_counter_green_sync0 <= 0;
wr_counter_green_sync1 <= 0;
end
else begin
wr_counter_green_sync0 <= wr_counter_green;
wr_counter_green_sync1 <= wr_counter_green_sync0;
end
end
//格雷码转回二进制
genvar i;
generate
for(i=0;i<=ADDR_WIDTH;i = i + 1) begin : green_to_bin
if(i == ADDR_WIDTH) begin
assign wr_counter_bin[ADDR_WIDTH] = wr_counter_green_sync1[ADDR_WIDTH];
assign rd_counter_bin[ADDR_WIDTH] = rd_counter_green_sync1[ADDR_WIDTH];
end
else begin
assign wr_counter_bin[i] = wr_counter_bin[i + 1] ^ wr_counter_green_sync1[i];
assign rd_counter_bin[i] = rd_counter_bin[i + 1] ^ rd_counter_green_sync1[i];
end
end
endgenerate
//计算当前的货量
//这一大坨就是算"仓库里现在存了几个字"(0~32)。分两种情况:
// - 如果最高位相同(读没落后一整圈):货量 = 写地址 − 读地址。
// - 如果最高位不同(写绕了一圈):货量 = (32 − 读地址) + 写地址。
assign wr_counter_program_cal = rd_counter_bin[ADDR_WIDTH] == wr_counter[ADDR_WIDTH] ?
wr_counter[ADDR_WIDTH - 1:0] - rd_counter_bin[ADDR_WIDTH - 1:0] :
ADDR_LEN - rd_counter_bin[ADDR_WIDTH - 1:0] + wr_counter[ADDR_WIDTH - 1:0];
//读过程和写过程对称
assign rd_counter_program_cal = rd_counter[ADDR_WIDTH] == wr_counter_bin[ADDR_WIDTH] ?
wr_counter_bin[ADDR_WIDTH - 1:0] - rd_counter[ADDR_WIDTH - 1:0] :
ADDR_LEN - rd_counter[ADDR_WIDTH - 1:0] + wr_counter_bin[ADDR_WIDTH - 1:0];
//快满/空 判断并置位程序
always@(posedge wr_clk) begin
if(wr_reset) begin
wr_full_program <= 0;
end
else if(wr_counter_program_cal >= PROGRAM_FULL_SET - 1) begin // 快写满,拉高快满信号
wr_full_program <= 1;
end
else if(wr_counter_program_cal <= PROGRAM_FULL_CLR - 1) begin //正常,置0
wr_full_program <= 0;
end
end
always@(posedge rd_clk) begin
if(rd_reset) begin
rd_empty_program <= 0;
end
else if(rd_counter_program_cal <= PROGRAM_EMPTY_SET - 1) begin //快空,拉高快空信号
rd_empty_program <= 1;
end
else if(rd_counter_program_cal >= PROGRAM_EMPTY_CLR - 1) begin //正常。置0
rd_empty_program <= 0;
end
end
//判断满/空
/*
- 满:最高位不同、低 5 位相同(写绕了整整一圈追上读)。
- 空:最高位相同、低 5 位也相同(完全相等)。
*/
assign wr_full = (wr_counter[ADDR_WIDTH] != rd_counter_bin[ADDR_WIDTH]) &&
(wr_counter[ADDR_WIDTH - 1:0] == rd_counter_bin[ADDR_WIDTH - 1:0]);
assign rd_empty = (rd_counter[ADDR_WIDTH] == wr_counter_bin[ADDR_WIDTH]) &&
(rd_counter[ADDR_WIDTH - 1:0] == wr_counter_bin[ADDR_WIDTH - 1:0]);
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
`timescale 1ns / 1ns
// Description: Clock and reset system
// Author: JerryTech
// License: GPLv3
module clock_tree(
input wire fpga_clk,
output wire usb_clk,
output wire usb_resetn,
output wire audio_clk,
output wire audio_resetn
);
wire clk72mhz;
wire aud_clk;
reg clk_locked;
usb_rPLL u_usb_pll (
.clkout ( usb_clk ), //50MHz → 60MHz
.clkin ( fpga_clk ),
.lock ( usb_resetn )
);
audio_master_rPLL your_instance_name(
.clkout ( clk72mhz ), //:60MHz → 72MHz
.lock ( ),
.clkin ( usb_clk ) //input clkin
);
audio_rPLL aud_pll(
.clkout ( ), //output clkout
.clkoutd ( audio_clk ), //// 用这个分频输出 3.072MHz
.lock ( audio_resetn ), //output lock
.clkin ( clk72mhz ) //input clkin
);
//lock 是 PLL 的 “锁定指示” 信号。它的作用是告诉 FPGA 的其他部分:“我这个 PLL 已经稳定了,输出的时钟可以放心用了
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
`timescale 1ns / 1ns
// Description: volume control
// Author: JerryTech
// License: GPLv3
//SEG decoder refers to:https://wiki.lckfb.com/zh-hans/fpga-ljpi/beginner/seg.html
module volume_control #(
parameter SYS_FREQ = 'd3072000,
parameter KEY_POLARITY = 0
)
(
input wire sys_clk, //时钟(3.072MHz)
input wire reset,
input wire key_vol_up, //音量+ 按键 低有效
input wire key_vol_down, //5 stages,0 is mute
input wire [31: 0] audio_data_in,
output reg [31: 0] audio_data_out,
output reg [7 : 0] seg_display //Common anode seg display
);
localparam DEBOUNCE_TIME = 40; //消抖参数
wire volume_up_pressed;
wire volume_down_pressed;
reg [3:0] volume_level;
// 两个按键消抖模块
key_debounce #(
.SYS_FREQ ( SYS_FREQ ),
.KEY_POLARITY ( KEY_POLARITY ),
.DEBOUNCE_TIME ( DEBOUNCE_TIME )
) u_key_up
(
.sys_clk (sys_clk),
.reset (reset),
.key_in (key_vol_up),
.key_debounce_out (volume_up_pressed)
);
key_debounce #(
.SYS_FREQ ( SYS_FREQ ),
.KEY_POLARITY ( KEY_POLARITY ),
.DEBOUNCE_TIME ( DEBOUNCE_TIME )
) u_key_down
(
.sys_clk (sys_clk),
.reset (reset),
.key_in (key_vol_down),
.key_debounce_out (volume_down_pressed)
);
always@(posedge sys_clk) begin
if(reset) begin
volume_level <= 8; //初始音量最高
end
else if(volume_down_pressed)begin //音量减
volume_level <= volume_level == 0 ? 0 : volume_level - 1;
end
else if(volume_up_pressed)begin
volume_level <= volume_level == 8 ? 8 : volume_level + 1;
end
end
//有符号位的移位操作
always@(posedge sys_clk) begin
if(reset) begin
audio_data_out <= 0;
end
else begin
case(volume_level)
'd0: begin
audio_data_out[31:16] <= 0;
audio_data_out[15: 0] <= 0; //右声道(高16位
end
'd1: begin
audio_data_out[31:16] <= {{3{audio_data_in[31]}},audio_data_in[31:19]}; //1 / 8 volume
audio_data_out[15: 0] <= {{3{audio_data_in[15]}},audio_data_in[15: 3]};
end
'd2: begin
audio_data_out[31:16] <= {{2{audio_data_in[31]}},audio_data_in[31:18]};//2 / 8 volume
audio_data_out[15: 0] <= {{2{audio_data_in[15]}},audio_data_in[15: 2]};
end
'd3: begin
audio_data_out[31:16] <= {{2{audio_data_in[31]}},audio_data_in[31:18]} + {{3{audio_data_in[31]}},audio_data_in[31:19]};//3 / 8 volume
audio_data_out[15: 0] <= {{2{audio_data_in[15]}},audio_data_in[15: 2]} + {{3{audio_data_in[15]}},audio_data_in[15: 3]};
end
'd4: begin
audio_data_out[31:16] <= {{1{audio_data_in[31]}},audio_data_in[31:17]};//4 / 8 volume
audio_data_out[15: 0] <= {{1{audio_data_in[15]}},audio_data_in[15: 1]};
end
'd5: begin
audio_data_out[31:16] <= {{1{audio_data_in[31]}},audio_data_in[31:17]} + {{3{audio_data_in[31]}},audio_data_in[31:19]};//5 / 8 volume
audio_data_out[15: 0] <= {{1{audio_data_in[15]}},audio_data_in[15: 1]} + {{3{audio_data_in[15]}},audio_data_in[15: 3]};
end
'd6: begin
audio_data_out[31:16] <= {{1{audio_data_in[31]}},audio_data_in[31:17]} + {{2{audio_data_in[31]}},audio_data_in[31:18]};//6 / 8 volume
audio_data_out[15: 0] <= {{1{audio_data_in[15]}},audio_data_in[15: 1]} + {{2{audio_data_in[15]}},audio_data_in[15: 2]};
end
'd7: begin
audio_data_out[31:16] <= {{1{audio_data_in[31]}},audio_data_in[31:17]} + {{2{audio_data_in[31]}},audio_data_in[31:18]} + {{3{audio_data_in[31]}},audio_data_in[31:19]};//7 / 8 volume
audio_data_out[15: 0] <= {{1{audio_data_in[15]}},audio_data_in[15: 1]} + {{2{audio_data_in[15]}},audio_data_in[15: 2]} + {{3{audio_data_in[15]}},audio_data_in[15: 3]};
end
'd8: begin
audio_data_out[31:16] <= audio_data_in[31:16];
audio_data_out[15: 0] <= audio_data_in[15: 0];
end
default: begin
audio_data_out[31:16] <= 0;
audio_data_out[15: 0] <= 0;
end
endcase
end
end
//Seg display. Refers to : https://wiki.lckfb.com/zh-hans/fpga-ljpi/
always @(posedge sys_clk) begin
if(reset)
seg_display <= 8'hFF; //清理 零
else begin
case(volume_level) // 根据number的值选择对应的7段显示编码
4'd0: seg_display <= 8'hC0; // 显示数字0
4'd1: seg_display <= 8'hF9; // 显示数字1
4'd2: seg_display <= 8'hA4; // 显示数字2
4'd3: seg_display <= 8'hB0; // 显示数字3
4'd4: seg_display <= 8'h99; // 显示数字4
4'd5: seg_display <= 8'h92; // 显示数字5
4'd6: seg_display <= 8'h82; // 显示数字6
4'd7: seg_display <= 8'hF8; // 显示数字7
4'd8: seg_display <= 8'h80; // 显示数字8
4'd9: seg_display <= 8'h90; // 显示数字9
default: ; // 默认情况下不进行任何操作
endcase
end
end
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
`timescale 1ns / 1ns
// Description: LED Driver
// Author: JerryTech
// License: GPLv3
module led_driver #(
parameter SYS_FREQ = 'd3072000,
parameter BREATH_CYCLE_SEC = 'd4 //One cycle need secs
)
(
input wire sys_clk,
input wire reset,
input wire fifo_empty,
input wire usb_rstn,
output reg led_red,
output reg led_green,
output reg led_blue
);
localparam PWM_DIV = 'd1024; // PWM 的分辨率:把亮度分成 1024 级
localparam PWM_CLK_DIV = SYS_FREQ * BREATH_CYCLE_SEC / 2 / PWM_DIV / PWM_DIV ; // // 计算分频系数。本工程 SYS_FREQ=3072000,结果约等于 5
reg [9 :0] clk_div_counter; // 计数器1:把 3.072MHz 再分慢
reg [9 :0] pwm_value_counter; // 计数器2:从 0 数到 1023,代表 PWM 一个周期
reg [9 :0] pwm_value_adder[2:0]; // 3 个通道各自的"亮度目标值"(0~1022)
reg [2 :0] pwm_add_sub_dir; // 每个通道当前是在"变亮"还是"变暗"
// 计数器1:clk_div_counter 一直数,数到 PWM_CLK_DIV-1 就归零(分频)
always@(posedge sys_clk) begin
if(reset) begin
clk_div_counter <= 0;
end
else begin
clk_div_counter <= clk_div_counter == PWM_CLK_DIV - 1 ? 0 : clk_div_counter + 1;
end
end
// 计数器2:每当计数器1 归零时,计数器2 才 +1;数到 1023 归零
always@(posedge sys_clk) begin
if(reset) begin
pwm_value_counter <= 0;
end
else if(clk_div_counter == PWM_CLK_DIV - 1) begin
pwm_value_counter <= pwm_value_counter == PWM_DIV - 1 ? 0 : pwm_value_counter + 1;
end
end
// 3 个通道(红/绿/蓝)各自的"亮度目标值",相位错开,才好看
// pwm_value_adder自动加,加到1023,则开始减,减到0开始加
genvar i;
generate
for(i = 0; i < 3; i = i + 1) begin:three_phase_gen
always@(posedge sys_clk) begin
if(reset) begin
pwm_value_adder[i] <= i * PWM_DIV / 3;
pwm_add_sub_dir[i] <= 0;
end
else if((pwm_value_counter == PWM_DIV - 1) && (clk_div_counter == PWM_CLK_DIV - 1)) begin
pwm_add_sub_dir[i] <= pwm_value_adder[i] == PWM_DIV - 2 ? 1 : pwm_value_adder[i] == 0 ? 0 : pwm_add_sub_dir[i];
pwm_value_adder[i] <= pwm_add_sub_dir[i] ? pwm_value_adder[i] - 1 : pwm_value_adder[i] + 1;
end
end
end
endgenerate
always@(posedge sys_clk) begin
if(reset) begin
led_red <= 1'b0; //default red led
led_green <= 1'b1;
led_blue <= 1'b1;
end
else if(~fifo_empty) begin // FIFO 非空 → 有数据在等待播放 此时 LED 显示 PWM 呼吸灯效果,三种颜色交替亮灭。
led_red <= pwm_value_counter >= pwm_value_adder[0] ? 1'b0 : 1'b1;
led_green <= pwm_value_counter >= pwm_value_adder[1] ? 1'b0 : 1'b1;
led_blue <= pwm_value_counter >= pwm_value_adder[2] ? 1'b0 : 1'b1;
end
else if(usb_rstn) begin
led_red <= 1'b1;
led_green <= 1'b1;
led_blue <= 1'b0; //// USB 已连接,蓝色常亮
end
else begin
led_red <= 1'b0; //默认红色常亮
led_green <= 1'b1;
led_blue <= 1'b1;
end
end
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
`timescale 1ns / 1ns
// Description: key debounce
// Author: JerryTech
// License: GPLv3
module key_debounce #(
parameter SYS_FREQ = 100_000_000,
parameter KEY_POLARITY = 0,//0 is low active,1 is high active
parameter DEBOUNCE_TIME = 40 //MS
)
(
input wire sys_clk,
input wire reset,
input wire key_in,
output reg key_debounce_out //消抖后的单拍脉冲(高有效
);
localparam DEBOUNCE_CYCLE = DEBOUNCE_TIME * SYS_FREQ / 1000; // 40ms 需要多少个时钟周期 = 40ms × 时钟频率 / 1000 122880 个周期
localparam COUNTER_WIDTH = $clog2(DEBOUNCE_CYCLE); // $clog2(x) = 表示 x 需要几位二进制。122880 需要 17 位,所以 COUNTER_WIDTH=17
reg [COUNTER_WIDTH: 0] key_debounce_counter; // 计数器,18 位
reg key_lock; // "锁定"标志:按一次只响一次
wire key_pressed = KEY_POLARITY == 0 ? ~ key_in : key_in; //按键按下时key press = 1
always@(posedge sys_clk) begin
if(reset) begin
key_debounce_counter <= 'd0;
key_debounce_out <= 1'd0;
key_lock <= 1'b0;
end
else if(key_pressed) begin //按键按下
key_lock <= key_debounce_counter == DEBOUNCE_CYCLE - 1 ? 1'b1 : 1'b0; //满40ms keylock为1
key_debounce_out <= key_debounce_counter == DEBOUNCE_CYCLE - 1 ? ~ key_lock : 1'b0; //记满的时候
key_debounce_counter <= key_debounce_counter == DEBOUNCE_CYCLE - 1 ? key_debounce_counter : key_debounce_counter + 1; //计满后保持不变
end
else begin
key_debounce_counter <= 'd0;
key_debounce_out <= 1'd0;
key_lock <= 1'b0;
end
end
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
注
6.2、音频驱动模块 LSBJ_audio_driver.v
这是把并行数据变成 PT8211 三线时序的核心模块,也是"时序说明"那一节的代码来源。
用一个 7 位计数器 counter 从 95 减到 0,一圈 96 拍:
三根信号全部由 counter 推出来:
6.3、 异步 FIFO async_fifo.v
注:FIFO写端(USB 端) 写端在 60MHz 域将并行音频数据写入 异步FIFO
而 FIFO 读端 在 3.072MHz 域
核心是格雷码 + 两级寄存器:
// 指针转格雷码(相邻两个数只差 1 位,跨时钟域时不会读到中间态)
assign wr_counter_green = wr_counter ^ (wr_counter >>> 1);
// 两级同步,消除亚稳态
rd_counter_green_sync0 <= rd_counter_green;
rd_counter_green_sync1 <= rd_counter_green_sync0;2
3
4
5
6
判断满/空靠指针的"最高位 + 低 5 位":
- 空:读、写指针完全相等;
- 满:最高位不同、低 5 位相同
例化时覆盖参数为 32 深、32 位宽(一进一出就是左+右两个声道)。
异步 FIFO 的同步方式这里不再赘述,如果想具体学习异步 FIFO 的同步方式可以自行学习
6.5、音量控制 volume_control.v
音量分 9 档:0(静音)到 8(最大),上电默认最高 8 档。按键 vol_up/vol_down 调档。
调音量的本质是把音频数据进行除法分成1到8级。
由于乘法器要占很多资源这里用有符号数的右移公式去代替除法
数码管部分:一个 case 把 0~9 映射成共阳极 7 段码,显示当前档位。
6.6 、LED 驱动 led_driver.v
三颗 RGB LED 当状态灯,用 PWM 呼吸灯效果:
- FIFO 非空(正在放音):三色交替呼吸;
- USB 已连上:蓝灯常亮;
- 其他:红灯常亮。
else if(~fifo_empty) begin
led_red <= pwm_value_counter >= pwm_value_adder[0] ? 1'b0 : 1'b1; // PWM 调亮度
...
end
else if(usb_rstn) begin
led_red <= 1'b1; led_green <= 1'b1; led_blue <= 1'b0; // 蓝灯
end
//三路 pwm_value_adder 相位错开(i * PWM_DIV / 3),所以三色呼吸不同步,看起来是交替变色。2
3
4
5
6
7
8
9
6.7、按键消抖 key_debounce.v
机械按键按下瞬间会抖动几十毫秒,直接用会误判成按了好几下
消抖模块的思路:按键电平持续稳定 40ms才算一次有效按下
计满 40ms 才输出一个单拍脉冲,key_lock 保证按一次只出一次脉冲(不重复触发)。音量模块拿到这个干净脉冲再 +1 或 -1。
程序编写模块主要是对重要的源码进行一个注释讲解,要想运行程序还需要 USB 协议代码和 PLL IP 核配置。至于 PLL 时钟怎么配置可以自行打开工程,参照工程 IP 核配置
7、仿真编写
这里我们对程序进行仿真,主要是检验我们的,音频驱动模块的逻辑是否正确。
检验数据传入传出是否相同
具体文件在 sim 文件中的 tb.v
`timescale 1ns / 1ns
//////////////////////////////////////////////////////////////////////////////
// 测试对象:LSBJ_audio_driver —— 把并行左右声道数据,串行送给 PT8211
// 目的:验证"喂进去的数据"能不能被正确的一位一位串行输出(MSB 先行)
//
// 做法(对数据):
// 喂两个已知数 left=0xAAAA、right=0x5555
// 然后每一拍把 audio_din 实际吐出的位,跟期望的位逐一比对
// 错一个打印一行 [错误],最后打印 PASS / FAIL
//////////////////////////////////////////////////////////////////////////////
module tb_LSBJ_audio_driver;
// ============ 参数 ============
localparam DEF_LEN = 24;
localparam WIDTH = 16;
localparam LEFT = 16'hAAAA; // 左声道 1010_1010_1010_1010
localparam RIGHT = 16'h5555; // 右声道 0101_0101_0101_0101
// ============ 激励(给被测模块的输入) ============
reg audio_clk;
reg reset;
reg [WIDTH-1:0] left_data;
reg [WIDTH-1:0] right_data;
reg fifo_read_ready;
// ============ 观测(被测模块的输出) ============
wire fifo_read_en;
wire audio_ws;
wire audio_bck;
wire audio_din;
// ============ 实例化被测模块 ============
LSBJ_audio_driver #(
.DEF_LEN (DEF_LEN),
.WIDTH (WIDTH),
.FIFO_DELAY_MIN (4)
) dut (
.audio_clk (audio_clk),
.reset (reset),
.left_data (left_data),
.right_data (right_data),
.fifo_read_ready (fifo_read_ready),
.fifo_read_en (fifo_read_en),
.audio_ws (audio_ws),
.audio_bck (audio_bck),
.audio_din (audio_din)
);
// ============ 时钟:周期 20ns ============
initial audio_clk = 1'b0;
always #10 audio_clk = ~audio_clk;
// ============ 自动比对 ============
reg [6:0] prev_counter; // 上一拍的 counter
reg [6:0] check_count; // 已检查拍数(首拍跳过)
reg expected; // 期望位
integer error_cnt; // 错误计数
always @(posedge audio_clk) begin
#1; // 等非阻塞赋值更新完,读到本拍新值
if (!reset && check_count > 0) begin
// ---- 算期望位 ----
if (prev_counter >= 1 && prev_counter <= 2*WIDTH) begin
expected = left_data[(prev_counter - 1) >> 1]; // 左声道
end
else if (prev_counter >= 2*DEF_LEN+1 && prev_counter <= 2*(WIDTH+DEF_LEN)) begin
expected = right_data[(prev_counter - 2*DEF_LEN - 1) >> 1]; // 右声道
end
else begin
expected = 1'b0; // 空档
end
// ---- 比对 ----
if (audio_din !== expected) begin
error_cnt = error_cnt + 1;
$display(" [错误] counter=%0d 实际=%b 期望=%b", prev_counter, audio_din, expected);
end
end
prev_counter <= dut.counter;
check_count <= check_count + 1;
end
// ============ 主流程 ============
initial begin
reset = 1'b1;
left_data = LEFT;
right_data = RIGHT;
fifo_read_ready = 1'b1;
error_cnt = 0;
prev_counter = 0;
check_count = 0;
repeat (5) @(posedge audio_clk); // 复位 5 拍
reset = 1'b0;
repeat (96 * 3) @(posedge audio_clk); // 跑 3 个完整周期(每周期 96 拍)
#5; // 等最后一拍比对完成
if (error_cnt == 0)
$display("\n========== PASS : 数据全部正确 ==========\n");
else
$display("\n========== FAIL : 共 %0d 个错误 ==========\n", error_cnt);
$finish;
end
endmodule2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
仿真模块编写好后接下来我们在 modelisim 中进行仿真
我这里用的是 modelsim SE 2020年版本的,当然用其他仿真软件和版本也可以
将我们的 LSBJ_audio_driver.v 和 tb.v 加入工程编译之后即可在软件终端查看检验结果和波形,如图:
在 transcript 中看到打印的这个消息 ‘PASS' 表示我们以及检验成功而且没有错 后面的乱码是中文大家不用在意,再添加波形。
可以看到 bck ws 信号都正常,bck 走一拍写一位数据counter 一个周期从 05 递减到 0 说明仿真没问题。
8、I/O 绑定
引脚图如下
引脚较多,大家可以直接去源工程里面复制.cst文件
9、程序下载
程序下载分为以下几步:
第一步
- 先将 PT8211 连接到我们逻辑派的拓展引脚上 PT8211 背面有引脚的端口名称 我们将 5V 和 GND 对应着连接到板子的左下角 如图
第二步
- 用数据线连接下载器,将我们的程序固化到板子里面。
第三步
- 固化完成后,拔下数据线,将数据线连接到板子上的 USB 口(不是下载器上的 USB 口),如图:
然后我们即可在电脑右下角喇叭里选择逻辑派作为音频输出,如图:
注意:连接到电脑上后电脑右下角可能会弹出一个‘无法识别的设备的警告’
这是由于上电之后 flash 需要时间把程序写回板子里,这个时间电脑没有识别出设备就会报错, 等待一会自己会消失,不必理会。
最后
连接上之后无需下载驱动,会自动输出电脑的声音,板子上的 key 1,key 2 分别用来增加,减少音量