这一篇在干嘛?
这是前面章节的实战应用章:用两个真实的数字音频接口——源同步的 I2S 和异步的 SPDIF——演示”异步信号恢复 + 数据重同步”的完整设计过程。看完你就能体会:接口有没有随路时钟,直接决定了接收架构的复杂度天差地别。
7.0 背景与目标
SPDIF(Sony/Philips Digital Interface Format,索尼/飞利浦数字接口格式)和 I2S(Inter-IC Sound,芯片间音频总线)是消费电子行业广泛使用的两个数字音频传输标准,目的是在芯片/设备之间传数字音频,从而避免设备间跑模拟信号——数字信号在到达本地 DAC(数模转换器)之前保持数字形态,抗噪声和信号劣化能力强得多。
本章目标:讲清两种接口接收器的架构,并分析异步信号的恢复方法与音频数据的重同步——正好把第 6 章的跨时钟域技术用起来。
7.1 I2S:源同步接口
协议
I2S 设计用于最高 192 kHz 采样率的音频传输,方式是源同步(source-synchronous):时钟随数据一起传输。有了随路时钟,收发两端就不必共享系统时钟。数据样本 16~24 位,无论位宽多少都归一化到满幅幅度;与 SPDIF 不同,I2S 的不同字长不能随意互换——接收端必须预先知道字长。
I2S 的核心设计问题就一个:把样本从源时钟域传到本地时钟域。因为数据随源时钟一起来,可以先用源时钟轻松重建数据,再重同步——典型的”先同步恢复、末端跨域”结构。
I2S 是极简的三线同步协议:
- LRCK(左右声道选择):低电平 = 左声道数据,高电平 = 右声道数据;
- BCK(位时钟):就是那个源同步时钟;
- DATA(串行音频数据):来自音频编解码器的原始样本位,与 BCK 同步。
时序波形:

图 7-1:I2S 时序。LRCK 定义声道(低=左、高=右),BCK 把 DATA 线上的逻辑值打拍移入。LRCK 和 DATA 的所有翻转都发生在 BCK 的下降沿——这样双向都能容忍少量偏斜而不违反建立/保持时间。MSB 到 LSB 的长度由字长决定(本实现固定 16 位)。很多 I2S 接收器还有左右对齐等扩展模式,本章只考虑”真 I2S”格式。
硬件架构
架构非常简单:

图 7-2:I2S 架构。BCK 的每个上升沿把 DATA 移入移位寄存器;检测到 LRCK 翻转时,按 LRCK 极性把移位寄存器内容装载进对应输出寄存器。整个 I2S 电路用 BCK 当系统时钟,形成完全同步的接收器;数据锁入输出寄存器之后,才需要传给本地系统时钟域——跨域发生在 I2S 数据恢复的最末端。
完整实现如下。代码前说明:BCK 域内完成边沿检测、移位捕获、装载,SysClk 域只用延迟的 Strobe 做末端重同步。
module I2S(
output reg oStrobeL, oStrobeR,
output reg [23:0] oDataL, oDataR,
input iBCK, // bit clock
input iSysClk, // local system clock
input iDataIn,
input iLRCK);
reg DataCapture;
reg rdatain;
// registers to capture input data on rising and falling
// edges of clock
reg [23:0] Capture;
// strobes for valid data
reg StrobeL, StrobeR;
reg [2:0] StrobeDelayL, StrobeDelayR;
reg [23:0] DataL, DataR;
reg LRCKPrev;
reg [4:0] bitcounter;
reg triggerleft, triggerright;
reg LRCKRise, LRCKFall;
wire [23:0] DataMux;
// detect edges of LRCK
assign LRCKRise = iLRCK & !LRCKPrev;
assign LRCKFall = !iLRCK & LRCKPrev;
// assuming 16 bit data
assign DataMux = {Capture[15:0], 8'b0};
always @(posedge iBCK) begin
DataCapture <= (bitcounter != 0);
triggerleft <= LRCKRise;
triggerright <= LRCKFall;
rdatain <= iDataIn;
// for detecting edges of LRCK
LRCKPrev <= iLRCK;
// capture data on rising edge, MSB first
if(DataCapture)
Capture[23:0] <= {Capture[22:0], rdatain};
// counter for left justified formats
if(LRCKRise || LRCKFall)
bitcounter <= 16;
else if(bitcounter != 0)
bitcounter <= bitcounter - 1;
// Load data into register for resynchronization
if(triggerleft) begin
DataL[23:0] <= DataMux;
StrobeL <= 1;
end
else if(triggerright) begin
DataR[23:0] <= DataMux;
StrobeR <= 1;
end
else begin
StrobeL <= 0;
StrobeR <= 0;
end
end
// resynchronize to new clock domain
always @(posedge iSysClk) begin
// delay strobes relative to data
StrobeDelayL <= {StrobeDelayL[1:0], StrobeL};
StrobeDelayR <= {StrobeDelayR[1:0], StrobeR};
// upon the rising edge of the delayed strobe
// the data has settled
if(StrobeDelayL[1] & !StrobeDelayL[2]) begin
oDataL <= DataL; // load output
oStrobeL <= 1; // single cycle strobe in new domain
end
else
oStrobeL <= 0;
if(StrobeDelayR[1] & !StrobeDelayR[2]) begin
oDataR <= DataR; // load output
oStrobeR <= 1; // single cycle strobe in new domain
end
else
oStrobeR <= 0;
end
endmodule代码点评,分三步看:
第一步:检测 LRCK 翻转,用来清零位计数器(LRCKRise = iLRCK & !LRCKPrev 是标准的同步边沿检测写法):

图 7-3:LRCK 检测。全同步实现,无毛刺风险。
第二步:按位捕获进移位寄存器:

图 7-4:位捕获。Capture <= {Capture[22:0], rdatain} 左移进新位,MSB 先行;bitcounter 在 LRCK 翻转时重载 16,每拍递减,控制捕获窗口。
第三步:用 LRCK 触发信号(triggerleft/triggerright)把移位寄存器装载进输出寄存器(16 位数据经 DataMux = {Capture[15:0], 8'b0} 左对齐成 24 位),再重同步到本地时钟域。关键在最后那个 iSysClk 块:Strobe 先经过两拍延迟寄存器,用”延迟 1 拍为高、延迟 2 拍为低”检测 Strobe 的上升沿——此时数据早已稳定,oData 才装载。这就是”延迟有效位(delayed validity bit)“重同步法:让 Strobe 慢于数据,用 Strobe 的上升沿为新域生成单拍有效脉冲。
分析:三种重同步方案的取舍
I2S 这类源同步流的重同步有三种可选方案:
- 延迟有效位(本实现采用的);
- 输入流双触发器(第 6 章的 double flopping);
- FIFO 输出。
选型的首要考量是速度:本实现跑在音频位时钟上,最坏情况(192 kHz 采样率)约 12 MHz;如果改跑系统时钟,可能要满足几百 MHz 的时序。慢时钟域时序容易收敛,给设计者留出使用低面积技巧的空间,也让综合工具倾向于紧凑实现。代价是时钟分布和时序分析的复杂度增加(两个时钟都要管)。
什么情况下必须上 FIFO?当接收系统(I2S 接口后面的那一级)无法处理周期性的数据突发时——比如它通过共享总线访问本模块,数据不能一就绪就递上去。此时 FIFO 提供干净的跨域过渡,前提是总线端平均数据率大于音频数据率:

图 7-5:FIFO 同步方案。需要双端口 RAM 资源加控制逻辑。
三种拓扑的最终实现结果对比:
| 指标 | 输出双触发器 | 输入双触发器 | FIFO 输出 |
|---|---|---|---|
| 频率 | 197 MHz | 220 MHz | 164 MHz |
| Flip-flops(触发器) | 62 | 72 | 130 |
| LUTs(查找表) | 15 | 35 | 62 |
| Clock buffers(时钟缓冲) | 2 | 1 | 2 |
| Block RAMs(块 RAM) | 0 | 0 | 2 |
结论一目了然:FIFO 方案的资源开销(130 个 FF、62 个 LUT、2 块 BRAM)显著高于其他方案,除非系统必需,否则不应选用。
7.2 SPDIF:异步接口
协议
SPDIF 同样支持最高 192 kHz 采样率(此前长期上限是 96 kHz,很多设备发送前不会升采样超过它)。样本 16~24 位,归一化到满幅——多出来的位自动被当作更高精度而非更大幅度。从实现角度看,16 位字可视为低 8 位补零的 24 位字,捕获方式与字长无关(对比 I2S 必须先约定字长和格式)。
SPDIF 的核心设计问题是它的异步性:信号只用一根线传输,没有任何随路时钟可以同步——恢复时钟所需的全部信息都编码在串行流里,必须先重建时钟才能提取音频。
帧结构与位速率
每个音频样本打包成 32 位帧,含校验、有效位、用户位等附加信息(用户位甚至有效位在很多通用设备里都被忽略)。立体声每个采样周期要传两帧,因此位速率 = 32×2×Fs:
- 44.1 kHz 采样率 → 2.8224 MHz
- 96 kHz 采样率 → 6.144 MHz
32 位帧格式:
| 位 | 字段 |
|---|---|
| 31 | 奇偶校验(不含前导码) |
| 30 | 通道状态信息 |
| 29 | 子码数据 |
| 28 | 有效性(0 = 有效) |
| 27:4 | 音频样本(MSB 在位 27) |
| 3:0 | 前导码 |
本章实现只解码音频数据和前导码。
BMC 编码:一根线同时载时钟和数据
为了让接收器既能区分比特又能对帧重同步,SPDIF 用一种单线编码:BMC(Biphase Mark Code,双相标记码)。规则:每个位内至少翻转一次(0 或 1 都翻);区别在于——逻辑 0 每位翻 1 次,逻辑 1 每位翻 2 次:

图 7-6:BMC 编码示例。前两个波形是发送端看到的时钟和数据——在 I2S 这样的同步传输介质里直接随路发送即可;只有一根线时,数据就编码成第三行波形的 BMC 格式。时钟信息被编进数据流(每位至少一次翻转的约束),注意:产生 SPDIF 流的时钟必须是音频时钟的两倍频率,才能给每个逻辑 1 提供两次翻转。
前导码:故意违规实现帧同步
既然每个数据位必须每位至少一次翻转,SPDIF 就利用”每帧违规一次”来做帧同步——这就是前导码(preamble):
| 前导码 | 若上一电平=0 | 若上一电平=1 |
|---|---|---|
| 数据块起始的左声道 | 11101000 | 00010111 |
| 数据块中段的左声道 | 11100010 | 00011101 |
| 右声道 | 11100100 | 00011011 |
观察这些序列:每个前导码都通过连续 3 个时钟周期保持同一电平来违反翻转规则。检测到前导码,接收器就能把音频数据对齐到正确的声道。
硬件实现上,采样时钟必须足够快,才能既区分逻辑 0 和逻辑 1(脉宽差 2 倍),又区分逻辑 0 和前导码(脉宽差 1.5 倍)。
硬件架构

图 7-7:SPDIF 接收器基本架构。核心是脉宽检测逻辑:一个自由运行计数器,在 BMC 输入每次翻转时复位。翻转时把当前宽度与运行最小脉宽(1 位宽度参考值)比较判决:
- 宽度 > 2.5 倍最小脉宽 → BMC 违例,属于前导码;
- 宽度 > 1.5 倍最小脉宽 → 逻辑 0;
- 宽度 < 当前最小脉宽 → 更新最小脉宽参考,且判定音频数据因失锁而无效;
- 其余 → 逻辑 1 的半周期。
检测到逻辑 1 或 0,就把该位移入 24 位移位寄存器,等待与前导码同步;检测到前导码,说明上一帧已完整,可以按字段映射解码了。
完整实现如下。代码前说明:先输入重同步,再脉宽计数与参考宽度自适应,然后判决/移位/帧装载。
module spdif(
output reg oDatavalidL, oDatavalidR,
output reg [23:0] oDataL, oDataR,
input iClk, // main system clock used to sample spdif data
input iSPDIFin);
reg [2:0] inputsr; // input shift register
reg datatoggle; // register pulses high when data toggles
// counts the width between data transitions
reg [9:0] pulsewidthcnt;
// register to hold width between transitions
reg [9:0] pulsewidth;
reg [9:0] onebitwidth; // 1-bit width reference
// signals that pulsewidth has just become valid
reg pulsewidthvalid;
reg bitonedet; // detect logic-1 capture
reg newbitreg; // new data registered
reg [27:0] framecapture; // captured frame
reg preambledetect;
reg preambledetect;
reg preamblesyncen;
reg channelsel; // select channel based on preamble
reg [5:0] bitnum;
reg [10:0] onebitwidth1p5;
reg onebitload; // load 1-bit reference width
reg onebitupdown; // 1: reference width should increment
// width used for comparison against reference
reg [9:0] pulsewidthcomp;
reg onebitgood; // reference is equal to input width
reg preamblesync; // flags preamble in spdif stream
reg shiftnewdat; // ok to capture
// load data into output buffer
reg outputload, outputloadprev;
reg pulsewidthsmall, pulsewidthlarge;
reg [11:0] onebitwidth2p5;
wire trigviolation;
wire newbit; // raw data decoded from stream
// flag a violation in BMC code
assign trigviolation = {1'b0, pulsewidth[9:0], 1'b0} > onebitwidth2p5;
// if width is small, data is 1. Otherwise data is 0
assign newbit = ({pulsewidth[9:0],1'b0} < onebitwidth1p5[10:0]);
always @(posedge iClk) begin
inputsr <= {inputsr[1:0], iSPDIFin};
// shift data in
// trigger on change in data
datatoggle <= inputsr[2]^ inputsr[1];
// counter for pulse width
if (datatoggle) begin
// counter resets when input toggles
pulsewidth[9:0] <= pulsewidthcnt[9:0];
pulsewidthcnt <= 2;
end
else
pulsewidthcnt <= pulsewidthcnt + 2;
// width register will be valid 1 clock after the data toggles
pulsewidthvalid <= datatoggle;
// onebitload checks to see if input period is out of bounds
// current width is 1/2 1-bit width
pulsewidthsmall <= ({1'b0, onebitwidth[9:1]} > pulsewidth[9:0]);
// current width is 4x 1-bit width
pulsewidthlarge <= ({2'b0, pulsewidth[9:2]} > onebitwidth);
// load new reference if out of bounds
onebitload <= pulsewidthlarge || pulsewidthsmall;
// register width comparison value
if(!newbit)
pulsewidthcomp <= {1'b0, pulsewidth[9:1]};
else
pulsewidthcomp <= pulsewidth[9:0];
// checks to see if reference is equal to input width
onebitgood <= (pulsewidthcomp == onebitwidth);
// increment reference if input width is greater than reference
onebitupdown <= (pulsewidthcomp > onebitwidth);
// keep track of 1-bit width
// load reference if input width is out of bounds
if(onebitload)
onebitwidth <= pulsewidth[9:0];
else if(!onebitgood && pulsewidthvalid) begin
// adjust reference
if(onebitupdown)
onebitwidth <= onebitwidth+1;
else
onebitwidth <= onebitwidth-1;
end
// set onebitwidth*1.5 and onebitwidth*2.5
onebitwidth1p5 <= ({onebitwidth[9:0], 1'b0} + {1'b0, onebitwidth[9:0]});
onebitwidth2p5 <= ({onebitwidth[9:0], 2'b0} + {2'b0, onebitwidth[9:0]});
// preamblesync is valid only when last frame has completed
preamblesyncen <= (bitnum == 0) && datatoggle;
// trigger on preamble in spdif header if input width > 2.5*reference
preamblesync <= preamblesyncen && trigviolation;
// capture preamble
if(preamblesync)
preambledetect <= 1;
else if(preambledetect && pulsewidthvalid)
preambledetect <= 0;
// set channel
if(preambledetect && pulsewidthvalid)
channelsel <= !trigviolation;
else if(trigviolation && pulsewidthvalid)
channelsel <= 0;
newbitreg <= newbit;
// only trigger on a bit-1 capture every other transition
if(!newbitreg)
bitonedet <= 0;
else if(newbit && datatoggle)
bitonedet <= !bitonedet;
// set flag to capture data when bit-0 or bit-1 is valid
shiftnewdat <= pulsewidthvalid && (!newbit || bitonedet);
// shift register for capture data
if(shiftnewdat)
framecapture[27:0] <= {newbit, framecapture[27:1]};
// increment bit counter when new bit is valid
// reset bit counter when previous frame has finished
if(outputload)
bitnum <= 0;
else if(preamblesync)
bitnum <= 1;
else if(shiftnewdat && (bitnum != 0))
bitnum <= bitnum + 1;
// data for current frame is ready
outputload <= (bitnum == 31);
outputloadprev <= outputload;
// load captured data into output register
if(outputload & !outputloadprev) begin
if(channelsel) begin
oDataR <= framecapture[23:0];
oDatavalidR <= 1;
end
else begin
oDataL <= framecapture[23:0];
oDatavalidL <= 1;
end
end
else begin
oDatavalidR <= 0;
oDatavalidL <= 0;
end
endmodule实现逐步拆解
第一步:输入重同步。 SPDIF 输入相对本地系统时钟是异步的,先用第 6 章讲过的双触发器同步单比特流:

图 7-8:重同步 SPDIF 输入。代码里 inputsr 移位寄存器的前几级就是干这个的。
第二步:脉宽计数。 边沿检测用的是 inputsr 的位 2 和位 1(位 0、1 只用于时钟同步,位 2 用于检测翻转);同步后的翻转标志 datatoggle 复位脉宽计数器:

图 7-9:脉宽计数器。注意综合工具利用了触发器的复位和时钟使能引脚、消除了所有多路选择——这正是第 2 章讲过的技巧。
第三步:边界检查与参考宽度自适应。 判断当前脉宽是否超出可接受范围、是否需要重载 1 位宽度参考值(pulsewidthsmall 对应半位宽、pulsewidthlarge 对应 4 倍位宽的越界条件):

图 7-10:脉宽参考逻辑。onebitwidth 在越界时直接重载、界内且不匹配时 ±1 微调,实现对发送端时钟漂移的跟踪。
第四步:前导码检测。 把参考宽度乘 2.5 做帧同步判决:

图 7-11:前导码检测。注意 2.5 倍这个系数是用”左移+加法”最优实现的(onebitwidth2p5 = 4×w + w),不需要乘法器。
第五步:位判决。 在排除前导码后,用 1.5 倍阈值区分逻辑 0 和逻辑 1,决定移入 framecapture 的是 0 还是 1:

图 7-12:位检测。移入移位寄存器的值取决于当前脉宽与 1.5 倍位宽参考的比较。
最后一步:输出装载。 检测 outputload 的翻转(由位计数器 bitnum 到 31 触发),按 channelsel 选声道,把帧数据装进对应输出寄存器:

图 7-13:SPDIF 输出同步。
分析:采样率要多快才够?
用 BMC 这类编码重同步信号时,别无选择:必须在前端就采样信号并映射到本地时钟域,任何处理都只能在这最初的同步之后进行。而且采样 SPDIF 流的系统时钟必须显著快于 SPDIF 流的最小脉宽,脉宽阈值判决才有足够分辨率。在采样时钟与 SPDIF 流的所有相对相位下,都要满足:
- 逻辑 0 的脉宽介于最小脉宽(逻辑 1)的 1.5~3 倍之间;
- 前导码违例脉宽介于最小脉宽的 2.5~4 倍之间;
- 阈值两侧各留至少 2 个时钟周期的裕量,以吸收输入流或系统时钟的抖动(jitter)。

图 7-14:不同采样率下的判决窗口对比。可靠恢复的条件是采样率至少为最大时钟频率(逻辑 1 的完整周期)的 8 倍。
对 192 kHz 采样率,最坏情况下的时钟频率需求为:192 kHz × 64 × 8 = 98.304 MHz。以 Xilinx Spartan-3(10 ns 周期,留约 100 ps 抖动)为目标实现的结果:
| 指标 | 数值 |
|---|---|
| Frequency(频率) | 130 MHz |
| FFs(触发器) | 161 |
| LUTs(查找表) | 153 |
频率轻松达标,但注意:相对 I2S 这样的源同步系统,SPDIF 信号恢复所需的逻辑开销大得多——这就是”没有随路时钟”的代价。
7.3 两接口对比总结
| 维度 | I2S | SPDIF |
|---|---|---|
| 时钟 | 源同步(BCK 随路) | 异步(时钟编进 BMC 数据流) |
| 信号线 | 3 线(LRCK/BCK/DATA) | 1 线 |
| 字长 | 16~24 位,接收端须预知 | 16~24 位,捕获与字长无关 |
| 跨域方式 | 源时钟先恢复,末端延迟有效位重同步 | 前端直接用系统时钟采样判决 |
| 时钟要求 | 跑位时钟即可(约 12 MHz) | ≥ 8 倍最高位率(192k 时约 98.3 MHz) |
| 逻辑开销 | 小(62 FF / 15 LUT 级别) | 大(161 FF / 153 LUT) |
常见坑:SPDIF 采样时钟不够快,判决窗口被抖动吃掉
SPDIF 靠脉宽判决区分 0/1(差 2 倍)和 0/前导码(差 1.5 倍),判决窗口本来就不宽。如果采样时钟低于 8 倍最高位率,或没给阈值两侧留足 2 个时钟周期的抖动裕量,输入流或本地时钟的一点抖动就会让 0 被误判成 1、前导码被漏检,表现为”平时能放歌、偶尔爆音或失锁”。设计时先算最坏情况的位率,再反推采样时钟下限,不要拿”大概够快”的系统时钟直接采样。
通关标准:
学完本篇你应该能做到:
- 说清源同步(I2S)与异步(SPDIF)接口在时钟恢复上的本质区别,以及它如何决定接收架构复杂度;
- 画出 I2S 三线协议时序(LRCK 定声道、BCK 打拍、下降沿翻转容忍偏斜),并解释”延迟有效位”重同步的原理;
- 根据 32×2×Fs 计算任意采样率下的 SPDIF 位速率(44.1 kHz→2.8224 MHz,96 kHz→6.144 MHz);
- 解释 BMC 编码规则(0 翻 1 次、1 翻 2 次)和前导码”每帧故意违规 1.5 倍”的帧同步机制;
- 推导 SPDIF 采样时钟下限:8 倍最高位率,192 kHz 时为 98.304 MHz,并说明 1.5×/2.5× 两个阈值和 2 拍抖动裕量的作用。
自测:为什么 I2S 的 LRCK 和 DATA 都安排在 BCK 下降沿翻转?
因为接收端在 BCK 上升沿采样 DATA。让所有信号翻转都发生在下降沿,就在采样沿两侧各留出了半个周期的建立/保持裕量,双向都能容忍少量时钟偏斜而不违例。
自测:I2S 实现里 Strobe 为什么要延迟两拍后才用于装载输出数据?
数据先于 Strobe 装入输出寄存器;Strobe 延迟 1 拍、2 拍后检测其上升沿,此时数据早已在新时钟域稳定。“延迟有效位”保证输出侧逻辑只在数据完全稳定时才看到单拍有效脉冲,避免捕获到过渡态。
自测:SPDIF 的 32 位帧中,音频样本占哪些位?字长不同时捕获方式有变化吗?
音频样本占位 27:4(MSB 在位 27)。SPDIF 幅度归一化,多出的位被当作更高精度而非更大幅度,16 位字等价于低位补零的 24 位字,因此捕获方式与字长无关——对比 I2S 必须预先定义字长。
自测:前导码如何同时完成"帧同步"和"声道识别"两个任务?
前导码通过连续 3 个时钟周期同一电平违反 BMC 翻转规则(脉宽约 2.5 倍位宽),接收端据此锁定帧边界;而三种前导码模式(块首左声道、块中左声道、右声道)编码不同,检测到具体是哪种即可设置声道选择(
channelsel)。
自测:192 kHz 采样率的 SPDIF,系统采样时钟最低要多少?是怎么算出来的?
192 kHz × 64 × 8 = 98.304 MHz。其中 64 = 32 位 × 2 帧每采样周期(位率为音频时钟的 64 倍),8 = 判决 0/1(差 2 倍脉宽)与 0/前导码(差 1.5 倍脉宽)并留抖动裕量所需的最低采样倍数。低于这个速率,所有相对相位下都无法保证可靠判决。