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.
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.
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.westhost.com/project85.htm:
Implemented on a breadboard it looks like:
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.
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:
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
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.