Capturing S/PDIF Data


hamster
  • Capturing S/PDIF is a worthy project - you can listen to CDs on an FPGA, perform real time analysis of the signal, or use it as a handy data source for experimenting with DSP algorithms. It also can be used to provide access to the sub-code information embedded within the bit stream.

What is S/PDIF?

It is the digital audio output from CD's, PCs and other consumer devices.

In brief, it consists of a stream of subframes, each containing a header (equivalent in length to 4 bits), a 24 bit signed audio sample and 4 bits of subcode data.

The encoding is such that there each frame is encoded into 64 clock cycles (2 per bit). The signal always 'flips' between each data bit, and it also flips in the middle of a '0' bit. The binary value 11001010 will get encoded as either 11-00-10-10-11-01-00-10 or 00-11-01-01-00-10-11-01. So a 44,200Hz stream will actually consist of 32bit per subframe * 2 clocks per bit * 2 channels * 44,200 samples per second gives a S/PDIF signaling rate of 5,657,600Hz.

To provide synchronization of subframes, three header patterns are used - 00010111, 00011011, and 00011101 (and their inversions 11101000, 11100100, 11100010). Because these patterns break the usual rules of a signal change every other cycle it can be used to synchronize to the start of a subframe. The three different headers indicate which channel the subframe sample is for, and if the subframe is the start of a frames.

SPDIF_Frame.png

Electrical interface

Over coax, the signal is sent as a 0.5v peak-to-peak signal that needs conversion into LVTTL before it can be processed by an FPGA. I found this schematic at http://sound.westhos.../project85.htm:

Spdif_coax_lvttl.gif

Converting S/PDIF from coax to LVTTL

Implemented on a breadboard it looks like:

Spdif_coax_lvttl_breadboard.png

Converting S/PDIF from coax to LVTTL

I have tried implementing it using the FPGA's I/O pins, but it wasn't reliable - it needed a occasional poke of a finger to get it to successfully convert to TTL. I attribute this to the short circuit protection resistors on my FPGA development board, or maybe the Schottky characteristics on the FPGA's outputs.

How to capture the signal

First thing is to convert the signal into the FPGA's clock domain. I also use this to detect the flips in the input bitstream:

entity resync is
Port ( clk : in STD_LOGIC;
bitstream : in STD_LOGIC;
flipped : out STD_LOGIC;
synced : out STD_LOGIC);
end resync;

architecture Behavioral of resync is
signal ff1,ff2 : std_logic;
begin
flipped <= ff1 xor ff2;
synced <= ff2;

process (clk, pulse, ff1, ff2)
begin
if clk'event and clk = '1' then
ff2 <= ff1;
ff1 <= bitstream;
end if;
end process;
end Behavioral;

Failure to reclock caused me much grief.

One way to recover the S/PDIF data is to count the length of the pulses, giving pulses that are either one S/PDIF clock, two clock or three clocks in length. This works well, but needs a finite state machine to work out where the headers are and then to recover the data bits.

I chose to recover something close to the the sender's original clock, and use this to sample the signal into a 64 bit shift register the size of the frame. The highest 8 bits can be checked for a frame header, and the bits can be recovered by comparing even and odd positions in the shift register. Here's how the frame is assembled:

 entity frameCapture is
Port ( clk : in STD_LOGIC;
bitstream : in STD_LOGIC;
takeSample : in STD_LOGIC;
data : out STD_LOGIC_VECTOR (23 downto 0);
channelA : out STD_LOGIC;
dataValid : out std_logic);
end frameCapture;

architecture Behavioral of frameCapture is
signal frame : STD_LOGIC_VECTOR (63 downto 0) := x"0000000000000000";
begin
process(clk,bitstream)
begin
if clk'event and clk='1' and takeSample = '1' then
frame <= frame(62 downto 0) & bitstream;
end if;
end process;

process(frame)
begin
-- checking for a subframe header
dataValid <= '0';
channelA <= '0';
if frame(63 downto 56) = "00010111" or
frame(63 downto 56) = "11101000" then
dataValid <= '1';
channelA <= '1';
end if;

if frame(63 downto 56) = "00011101" or
frame(63 downto 56) = "11100010" then
dataValid <= '1';
channelA <= '1';
end if;

if frame(63 downto 56) = "00011011" or
frame(63 downto 56) = "11100100" then
dataValid <= '1';
channelA <= '0';
end if;
end process;

-- Recovery of data bits
data( 0) <= not frame(55) xor frame(54);
data( 1) <= not frame(53) xor frame(52);
...
data(21) <= not frame(13) xor frame(12);
data(22) <= not frame(11) xor frame(10);
data(23) <= not frame( 9) xor frame( 8);
end Behavioral;

So, how to regenerate something approaching the sender's clock? I chose to find the length of the shortest pulse, and then sample at 0.5x, 1.5x and 2.5x the minimum pulse length from a flip of the input signal. If the signal does not flip within four times the minimum sample time it indicates that minimum pulse length is incorrect, or the signal is no longer present.

 architecture Behavioral of reclock is
...
type reclock_reg is record
count : STD_LOGIC_VECTOR(9 downto 0);
takeSample : STD_LOGIC;
resetInputCounter : STD_LOGIC;
end record;
signal r : reclock_reg := ("0000000000",'0','0');
signal n : reclock_reg;
begin
...
process(flipped, r, oneAndAHalfPulse, twoAndAHalfPulse, fourPulse)
begin
n.count <= r.count+1;
n.takeSample <= '0';
n.resetInputCounter <= '0';

if n.count >= fourPulse then
n.resetInputCounter <= '1';
end if;

if n.count = halfPulse then
n.takeSample <= '1';
elsif n.count = twoAndAHalfPulse then
n.takeSample <= '1';
elsif n.count = oneAndAHalfPulse then
n.takeSample <= '1';
end if;

if flipped = '1' then
n.count <= "0000000001";
end if;
end process;

-- Assign next State
process (clk, n)
begin
if clk'event and clk = '1' then
r <= n;
end if;
end process;
end Behavioral;

Here is the original bitstream, and a second trace of the trigger used for sampling:

SPDIF_Sample_Clock.png

A S/PDIF frame, showing the Sampling trigger pulses

This is sub-optimal - if the minimum pulse is just under 5 FPGA cycles 2.5 x 4 cycles = 10 cycles - close enough that a sampling error can occur. Maybe sampling at (minimum pulse len-1), (2*minimum pulse len-1), (3*minimum pulse len-1) would be better when the FPGA clock rate is not many times that of the SPDIF signaling rate.

And that is pretty much it

Converting samples back to audio

Once you have the data, it's pretty simple to send it into a two generic 1bit DACs and listen to the sound. Just remember to convert the signed integer sample into an unsigned value for the DAC by inverting bit 15:

 entity dac16 is
Port ( clk : in STD_LOGIC;
data : in STD_LOGIC_VECTOR (15 downto 0);
dac_out : out STD_LOGIC);
end dac16;

architecture Behavioral of dac16 is
signal sum : STD_LOGIC_VECTOR (16 downto 0) := "01000000000000000";
begin
dac_out <= sum(16);
process (Clk, sum)
begin
if Clk'Event and Clk = '1' then
-- Don't forget to flip data(15) to convert it to an unsinged int value
sum <= ("0" & sum(15 downto 0)) + ("0" & (not data(15)) & data(14 downto 0));
end if;
end process;
end Behavioral;

Output was just through headphones connected between the DAC output and ground - not ideal, but as my development board has 220 Ohm resisters on all lines it couldn't harm it. A better way would be for a low pass filter, and a capacitor to block DC. All the same, the volume was loud enough that I had to use the inline volume control.



  Report Article



User Feedback


There are no comments to display.



Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.