这一篇在干嘛?

这是前面章节的实战应用章:用两个真实的数字音频接口——源同步的 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,所有翻转发生在 BCK 下降沿

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

硬件架构

架构非常简单:

图7-2 I2S 接收架构:BCK 作系统时钟的完全同步接收器,跨域发生在末端

图 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-3:LRCK 检测。全同步实现,无毛刺风险。

第二步:按位捕获进移位寄存器

图7-4 位捕获:BCK 上升沿把 DATA 移入 Capture 移位寄存器

图 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 这类源同步流的重同步有三种可选方案:

  1. 延迟有效位(本实现采用的);
  2. 输入流双触发器(第 6 章的 double flopping);
  3. FIFO 输出

选型的首要考量是速度:本实现跑在音频位时钟上,最坏情况(192 kHz 采样率)约 12 MHz;如果改跑系统时钟,可能要满足几百 MHz 的时序。慢时钟域时序容易收敛,给设计者留出使用低面积技巧的空间,也让综合工具倾向于紧凑实现。代价是时钟分布和时序分析的复杂度增加(两个时钟都要管)。

什么情况下必须上 FIFO?当接收系统(I2S 接口后面的那一级)无法处理周期性的数据突发时——比如它通过共享总线访问本模块,数据不能一就绪就递上去。此时 FIFO 提供干净的跨域过渡,前提是总线端平均数据率大于音频数据率:

图7-5 FIFO 同步:输出端挂 FIFO 吸收共享总线的访问延迟

图 7-5:FIFO 同步方案。需要双端口 RAM 资源加控制逻辑。

三种拓扑的最终实现结果对比:

指标输出双触发器输入双触发器FIFO 输出
频率197 MHz220 MHz164 MHz
Flip-flops(触发器)6272130
LUTs(查找表)153562
Clock buffers(时钟缓冲)212
Block RAMs(块 RAM)002

结论一目了然: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 编码示例:0 每位一次翻转,1 每位两次翻转

图 7-6:BMC 编码示例。前两个波形是发送端看到的时钟和数据——在 I2S 这样的同步传输介质里直接随路发送即可;只有一根线时,数据就编码成第三行波形的 BMC 格式。时钟信息被编进数据流(每位至少一次翻转的约束),注意:产生 SPDIF 流的时钟必须是音频时钟的两倍频率,才能给每个逻辑 1 提供两次翻转。

前导码:故意违规实现帧同步

既然每个数据位必须每位至少一次翻转,SPDIF 就利用”每帧违规一次”来做帧同步——这就是前导码(preamble):

前导码若上一电平=0若上一电平=1
数据块起始的左声道1110100000010111
数据块中段的左声道1110001000011101
右声道1110010000011011

观察这些序列:每个前导码都通过连续 3 个时钟周期保持同一电平来违反翻转规则。检测到前导码,接收器就能把音频数据对齐到正确的声道。

硬件实现上,采样时钟必须足够快,才能既区分逻辑 0 和逻辑 1(脉宽差 2 倍),又区分逻辑 0 和前导码(脉宽差 1.5 倍)。

硬件架构

图7-7 SPDIF 接收架构:脉宽计数 → 阈值判决 → 移位捕获 → 前导码同步 → 输出

图 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 输入的双触发器重同步

图 7-8:重同步 SPDIF 输入。代码里 inputsr 移位寄存器的前几级就是干这个的。

第二步:脉宽计数。 边沿检测用的是 inputsr 的位 2 和位 1(位 0、1 只用于时钟同步,位 2 用于检测翻转);同步后的翻转标志 datatoggle 复位脉宽计数器:

图7-9 SPDIF 脉宽计数器:输入翻转即复位,否则累加

图 7-9:脉宽计数器。注意综合工具利用了触发器的复位和时钟使能引脚、消除了所有多路选择——这正是第 2 章讲过的技巧。

第三步:边界检查与参考宽度自适应。 判断当前脉宽是否超出可接受范围、是否需要重载 1 位宽度参考值(pulsewidthsmall 对应半位宽、pulsewidthlarge 对应 4 倍位宽的越界条件):

图7-10 脉宽参考:越界重载,界内微调,自适应跟踪发送时钟

图 7-10:脉宽参考逻辑。onebitwidth 在越界时直接重载、界内且不匹配时 ±1 微调,实现对发送端时钟漂移的跟踪。

第四步:前导码检测。 把参考宽度乘 2.5 做帧同步判决:

图7-11 前导码检测:参考宽度×2.5(移位+加法实现),触发帧同步

图 7-11:前导码检测。注意 2.5 倍这个系数是用”左移+加法”最优实现的(onebitwidth2p5 = 4×w + w),不需要乘法器。

第五步:位判决。 在排除前导码后,用 1.5 倍阈值区分逻辑 0 和逻辑 1,决定移入 framecapture 的是 0 还是 1:

图7-12 位检测:脉宽 < 1.5 倍位宽判为 1,否则判为 0

图 7-12:位检测。移入移位寄存器的值取决于当前脉宽与 1.5 倍位宽参考的比较。

最后一步:输出装载。 检测 outputload 的翻转(由位计数器 bitnum 到 31 触发),按 channelsel 选声道,把帧数据装进对应输出寄存器:

图7-13 SPDIF 输出同步:帧完成后按声道装载并给出数据有效脉冲

图 7-13:SPDIF 输出同步。

分析:采样率要多快才够?

用 BMC 这类编码重同步信号时,别无选择:必须在前端就采样信号并映射到本地时钟域,任何处理都只能在这最初的同步之后进行。而且采样 SPDIF 流的系统时钟必须显著快于 SPDIF 流的最小脉宽,脉宽阈值判决才有足够分辨率。在采样时钟与 SPDIF 流的所有相对相位下,都要满足:

  • 逻辑 0 的脉宽介于最小脉宽(逻辑 1)的 1.5~3 倍之间;
  • 前导码违例脉宽介于最小脉宽的 2.5~4 倍之间;
  • 阈值两侧各留至少 2 个时钟周期的裕量,以吸收输入流或系统时钟的抖动(jitter)。

图7-14 SPDIF 采样率:8 倍最高时钟频率才能保证可靠判决

图 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 两接口对比总结

维度I2SSPDIF
时钟源同步(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、前导码被漏检,表现为”平时能放歌、偶尔爆音或失锁”。设计时先算最坏情况的位率,再反推采样时钟下限,不要拿”大概够快”的系统时钟直接采样。

通关标准:

学完本篇你应该能做到:

  1. 说清源同步(I2S)与异步(SPDIF)接口在时钟恢复上的本质区别,以及它如何决定接收架构复杂度;
  2. 画出 I2S 三线协议时序(LRCK 定声道、BCK 打拍、下降沿翻转容忍偏斜),并解释”延迟有效位”重同步的原理;
  3. 根据 32×2×Fs 计算任意采样率下的 SPDIF 位速率(44.1 kHz→2.8224 MHz,96 kHz→6.144 MHz);
  4. 解释 BMC 编码规则(0 翻 1 次、1 翻 2 次)和前导码”每帧故意违规 1.5 倍”的帧同步机制;
  5. 推导 SPDIF 采样时钟下限:8 倍最高位率,192 kHz 时为 98.304 MHz,并说明 1.5×/2.5× 两个阈值和 2 拍抖动裕量的作用。