Close
0%
0%

Propeller S/PDIF Receiver

Uncovering Subchannel Secrets

Similar projects worth following
If you're interested in digital audio, you know what the S/PDIF standard is. Also, if you've ever used any kind of digital recorder to record an audio CD, you know that the S/PDIF signal not only carries digital audio but also some extra data, such as markers that indicate the beginning of a new track, and information that prevents you from copying a tape that was copied from a CD.

For another project (which will be revealed when it's the right time), I need to decode the data that gets sent along with the audio in an S/PDIF signal. There are many S/PDIF receiver chips, and it's not too hard to analyze S/PDIF with an FPGA, but I wondered if it would be possible to do it with a Parallax Propeller. If it is, that would be great because I also want to visualize the data on a screen later on, and the Propeller can generate video.

The details about the S/PDIF protocol are standardized worldwide as IEC-60958. Unfortunately the IEC asks ridiculous amounts of money (hundreds of dollars) for a copy of the standard. As a hobbyist, I don't have wads of cash that big lying around.

The good news is that the Bureau of Indian Standard makes their own version of the standard available for non-commercial purposes, and it can be found at:

In this project, I want to find a way to decode the information from Part 3 (I don't have any devices that use part 4), but before I can get there, I have to also implement hardware and software to implement part 1.

There are many descriptions of the S/PDIF standard online such as this one: http://www.epanorama.net/documents/audio/spdif.html. It has also been described many times in magazines such as Elektor. The following is a description in my own words of the most important parts of the S/PDIF standard, based on the information in the BIS documents.

Read more »

  • Long Time No See!

    Jac Goudsmit05/20/2021 at 06:54 0 comments

    It's been a long time since I worked on this and my other projects. It's been very difficult to find time and space to work on things. But in the last few evenings I was successful in making small steps to achieve significant goals to advance some of my projects. Such as this one.

    Read more »

  • Intermezzo: 8 Megabits per Second Serial Transmitter

    Jac Goudsmit01/17/2018 at 05:32 0 comments

    I added a module to the project which is capable of generating fully formatted serial output. This is not really directly related to S/PDIF receiving of course but it helps to have a module that can quickly send debugging output to the serial port, e.g. for debugging the subchannel decoder as I tried to do in the previous log. And this should definitely be fast enough: the theoretical bitrate is a whopping 8 megabits per second, though the measured throughput at 3mbps (the highest speed that a Prop Plug and the Propeller Terminal will allow) is about 250,000 characters per second -- still pretty respectable compared to the 115200 maximum bit rate of the Full Duplex Serial module from the Parallax library.

    By "fully formatted", I mean:

    • Text: nul-terminated strings or fixed-length arrays of characters stored in the hub, unfiltered or filtered (i.e. unprintable characters replaced by a period)
    • Numbers: bytes, words or longwords (or arrays of any of those), in decimal (signed or unsigned), binary or hexadecimal.
    • Memory hexdump: combination of the address, hex bytes and filtered ASCII in the usual format address/hexdump/filtered-ascii

    The module can be easily controlled from Spin or from PASM: commands are passed through a single longword with bit fields. All you do is wait for the longword to be 0 (indicating the cog is done with the previous command) and set it to the value that represents the new command.

    I called the module TXX.spin and uploaded it to the Parallax Object Exchange (OBEX), at http://obex.parallax.com/object/870 (Update: OBEX is no longer available. You can get TXX from my Github repo at https://github.com/JacGoudsmit/TXX). I also wrote a post in the Parallax forums at https://forums.parallax.com/discussion/167981/txx-8mbps-serial-transmitter-with-extended-features-for-use-by-pasm-and-spin-code.

  • Racing the Subchannels

    Jac Goudsmit01/09/2018 at 09:08 0 comments

    It's been a while since I worked on the Propeller S/PDIF decoder, and there are really two reasons for that.

    One reason was that I was having trouble wrapping my head around what the best way would be to get the subchannel data out of the subchannel decoders. The other reason was that I realized that the maximum speed (115200bps) of the FullDuplexSerial module from the Propeller library would probably not be fast enough to keep up with the subchannels.

    Subchannels

    An audio CD plays 44100 frames of audio per second, each divided into two subframes: one for the left channel, one for the right channel. Besides the audio information, each subframe also has two bits that are used to store and transmit extra information about the CD, such as track markers. Each subchannel can be regarded as a bitstream that's multiplexed into the main data stream, so the subchannel data is transferred at 88200 bits per second for each subchannel on a CD. On a DAT tape that's recorded at 48kHz, the bitrate for the subchannels is 96 kilobits per second for each subchannel; on a DAB tuner that generates 32kHz audio, the subchannels run at 32 kbps each.

    There is some interesting information in the subchannels, which is what I want to get to in this project. But even though the subchannels generate a lot less data than the total volume of data coming in through S/PDIF, the maximum of 96 kbps is still a lot of data.

    When I wrote the subchannel decoder module for the project, I was mostly focused on demultiplexing the subchannel bits, and putting them in memory in some efficient way to analyze them by comparing them to known values, or whatever. Though the timing of the subchannel decoder is not nearly as tight as the biphase decoder, there's still too much to do to let the subchannel decoder take care of all the decoding. Besides, each type of medium encodes the data in a different way and I wanted to be able to use the module for the Channel Status subchannel as well as the User Data subchannel.

    For synchonization, the subchannels are organized in Blocks, and at every start of a block, the transmitter uses a special preamble. I wrote the original subchannel decoder to gather up all the bits in a block, and then copy all those bits to the hub at the end of the block. I used a counter to make it possible to see if a block didn't get decoded fast enough.

    Subchannel Decoder Rewrite

    The original implementation of the subchannel decoder turned out to not be very efficient or convenient. I decided to do a partial rewrite based on the following:

    • Instead of writing an entire block at the end of an incoming block, the code now copies the subdata to the hub one longword at a time. 32 divides evenly into 384 and 192 (the number of bits per block) so this is convenient.
    • Because of this, it was no longer possible for other cogs to recognize whether an incoming block of data was ready for processing. To fix this, I changed the code to use two buffers instead of one. One buffer gets written by the subchannel decoder while the other buffer is processed further, elsewhere. Also, to indicate that a buffer is ready for processing, I made the code use two locks (one per buffer).

    The subchannel module makes the pointers to the buffers and the lock numbers available, which makes it relatively easy for another cog to process the buffers using Spin or PASM (though Spin is likely to be much too slow for all but the simplest decoding). Such an analysis module (which will be written in the future) would:

    1. Set the lock for a buffer and check if the lock was taken. If no, repeat 1.
    2. Process the buffer
    3. Optionally set the lock again to see if there is an overrun situation
    4. Switch to the other lock and the other buffer.
    5. Repeat from step 1.

    Improving the Output Stage

    The second problem that I wanted to deal with, was that I had run into a bit of a wall: at 48000kHz,...

    Read more »

  • Progress?

    Jac Goudsmit06/23/2017 at 13:22 0 comments

    This is what the Channel Status subchannel looks like in binary:

    The code that I used for this output prints a block counter, followed by the Channel Status subchannel in binary (chronological order). As you can see, it contains the expected data: 11000011 in the second byte, because I connected my Propeller to a Digital Compact Cassette recorder which uses category code 110 0001L and this is a prerecorded tape so L=1. The rest of the data is all zeroes, which is a little boring but this at least demonstrates that I understand how block decoding and subchannel demultiplexing works.

    To get this output, I created a quick-and-dirty copy-paste module based on the status channel decoder, which doesn't just copy the status channel bits but entire blocks of subframes. This is what full blocks might look like in hexadecimal (actually the following output only shows the subframes for the left channel):

    The Channel Status subchannel is bit 30 of each subframe, in case you want to dig through the hexadecimal and make sure it checks out :-)

    So as part of narrowing down the problem with my subchannel decoder which only produces zeroes, I've proven that the data is there and has the expected format. Obviously there's something wrong with the PASM code that extracts the bits and sends them to the hub.

    I must be overlooking something in the PASM code or I'm misunderstanding how Spin works to copy the data.

    Hmmm...

    More research is needed :)

    UPDATE: Now we're talking!

    I found out what the problem was with the subchannel decoder: there was a rogue JMP in there that made another instruction unreachable that was needed to increase the destination address of the rotate-instruction. So yeah I saw a lot of zeroes because I had bit storage in the cog initialized to 0 and the $C3 in the second byte wasn't showing up because the first longword (that has the $C3 in it) was overwritten by 5 other ones by the time I got to see it.

    I rewrote the status channel module to be more universal (it can now also be used for the user data channel) and it looks like that works. Yay!

    However: remember I was hearing horrible distortion in the audio generator sometimes, that I could eliminate by resetting the Propeller? It looks like this is not a problem in the audio module, but in the biphase decoder. Apparently sometimes (and with sometimes I mean: way too often) it somehow locks onto the signal in the wrong way. I'm going to have to fix that because it's crucial for that to work. I must have overlooked some sort of corner case. I'm pretty confident I can fix it with a change to the initialization of the biphase decoder and/or preamble detector.

  • I Added a Status Subchannel Demultiplexer

    Jac Goudsmit06/21/2017 at 07:24 0 comments

    ...But it doesn't work.

    The Status Subchannel is a group of 192 bits that's continuously transmitted as part of the S/PDIF signal, interleaved with the audio. For every stereo sample, one bit of the status subchannel is added to the stream.

    There's supposed to be some interesting information in this subchannel, for example it can indicate what the type is of the device that the sound is coming from.

    But as you can see in the above screenshot, I'm not getting ANY data from my player. I'm not sure why.

    The User Data subchannel should prove more interesting for my "secret" final purpose of this project. For starters, it has twice as much data because there is a separate bit of information for each audio channel, and I know from looking at the logic analyzer earlier that there's definitely data in there. But when I changed the Assembly instruction that pulls the Channel Status bit from the subframes so that it puts the User Data bit (or any other bit for that matter), the output of the hex dump stays zero, though I've seen some glitches where I had some random data. That probably means I'm trashing memory somewhere...

    Hmmm...

    Well, it's night night time, I'll have to do a thorough code review tomorrow.

    PS: By the way, I decided that modifying the biphase decoder to write the subframes to a block of memory in the hub was too much work. The audio player is easiest to implement when it can just wait for the next subframe by checking PRADET, and it's not too difficult to implement the subchannel decoders that way too. And for other future purposes (CD+G decoders, I2S output generators, etc.) it's also not that hard to just wait for a single sample. Other advantages of this method are that the propagation delay stays low (one subframe delay instead of one block delay) and it keeps things easy for the Spin parts of the code, so I don't have to wrestle with partially filled circular buffers and other stuff. The buffer is one single longword and the synchronization method is the PRADET signal, and that's good enough for pretty much everything. If necessary, I can always use another cog to serialize each block of samples somehow.

  • Analog Audio Demo

    Jac Goudsmit06/18/2017 at 08:12 0 comments

    What's this, another change to the hardware? Well... Yes and no. I like the new Parallax FLiP for breadboard development but I wanted to do something today that actually makes the S/PDIF decoder do something that's (arguably) useful, so I connected a QuickStart board with a Human Interface Board for the Quickstart. The HIB board has filters and a connector to connect stereo headphones (just like the Propeller Demo board, by the way) and I wanted to write a small module that just grabs samples from the Biphase decoder and sends them to the headphones.

    It took me a while to get it to work, because apparently I had made a mistake and passed the pointer to a parameter variable (which was already a pointer) instead of passing the parameter directly.

    Because of this, I now have a sort-of sanity check that lets me know that the biphase data is decoded and processed correctly (well... at least the audio part of the subframes).

    The next step will be to change the Biphase decoder to write the data into a buffer instead of a single long word. This should come in useful for extracting subchannel data. And that's what this project is all about.

  • Biphase Decoder Working

    Jac Goudsmit06/12/2017 at 06:51 0 comments

    The idea that I presented in my previous log, to count pulses (instead of measuring time between pulses, or sampling for a second pulse in the middle of a bit) works great!

    The Biphase Decoder now consists of two cogs that take care of all the following:

    • Decoding the biphase bits in each subframe to regular binary values
    • Detecting the preamble to detect the start and end of each subframe
    • Decoding the preamble type to distinguish left and right channel samples
    • Decoding the preamble type to distinguish the first subframe of a block from all the other subframes
    • Storing a LONG word into the hub with all the information above, at the end of each subframe.

    That took quite a few smart tricks with Propeller Assembler (PASM). For example I found out that it was more efficient to set the channel output pin to 1 for the left channel and 0 for the right channel instead of the other way around, and I found out that it was more efficient to decode the biphase bits in one's complement.

    Let's have a look at some of the important parts of the code; for the full story, check out the source code on Github. The file biphasedec.spin has a lot of documentation at the top.

    Read more »

  • New Idea and a Change of Venue

    Jac Goudsmit06/03/2017 at 22:02 0 comments

    I haven't had time to work on this for a few days but I wanted to share a new idea of decoding the biphase input with the Propeller timers, and this picture:

    The photo shows a minor change of venue: I rebuilt the schematic on a different breadboard using the new Parallax FLiP module. This is a great new product ftom the producers of the Propeller (I'm not affiliated, just a fan as you may have noticed) that offers an entire Propeller circuit including USB to serial converter, 5 MHz crystal, and a great flexible power supply on a board that's the size of a DIP Propeller. They even had space to solder two LEDs onto pins 26 and 27 which can help greatly with debugging. Using the FLiP makes it much easier to put together a Propeller circuit because (unlike with the PE kit) you don't have to mess around with the power supply, the crystal, and/or a prop plug.

    Anyway, about that new idea I had: As I've mentioned before, the Propeller has to be able to process one bit of biphase data in 326ns if I want to make it work at 48kHz stereo sampling rate. That's about 6 assembler instructions. I already got it to (kinda) decode a signal by using two cogs (one for recovering the clock and one for measuring time) but I wasn't happy with the result.

    The last implementation of the biphase decoder counted flanks on the XORIN input which is better, but it was very difficult to get the timing right because of all the propagation delays, and I had the feeling that that wasn't going to be robust enough. For one thing, the timing would have to be adjusted to the input frequency, even if it changed only slightly.

    But earlier this week I had an epiphany: what if I just keep counting flanks from the beginning of a subframe to the end of a subframe, and never reset the counter? That would have some serious advantages!

    In the previous version of the code, I would read the timer/counter (PHSA register) at the start of every bit, and then reset it. But by the time that the timer actually gets reset, the next pulse is already almost coming in if the current bit is a 1. That's exactly what I didn't like about that code: I had to time the reset exactly right (within one 12.5ns Propeller clock cycle accuracy) so the timers wouldn't miss anything, and I had the feeling that this was pretty much impossible given the amount of jitter. That's not something I want the end user to have do (besides, it would probably be difficult to do while the system is running).

    I thought up a simple loop in PASM that goes about as follows:

    1. Set timer A to count positive edges on XORIN.
    2. Wait for a positive edge on XORIN using a WAITPxx instruction. This signifies the start of a new bit.
    3. Test the lsb of PHSA to find out if there was an odd or even number of transitions in the spdif input.
    4. Shift the odd/even result into a long word as a single bit.
    5. Check if a preamble was detected and jump out of the loop if so (I may do this a different way but that's not relevant for this discussion)
    6. By now, 6 instructions should have passed so jump back to the beginning of the loop (step 2). Alternatively, I may unroll the loop and just paste the above 32 times.

    At the end of the subframe, there are 32 bits of data available but each bit doesn't represent an actual data bit value but a record of the oddness of the total number of biphase flanks at the end of each bit time.

    On the SPDIF input, a zero-bit is represented as a single transition and a one-bit is represented as two transitions. So if the total number of transitions goes from even to odd or from odd to even in one bit-time, the encoded data bit must have been a 0 because one transition on the SPDIF line causes one pulse on the XORIN line and an odd number plus 1 is always an even number and vice versa. Similarly if the total number of positive edges on XORIN stayed even or stayed odd, the encoded data bit must have been a 1 because the oddness of a number doesn't change if you add (a multiple of) 2.

    I'll have to figure out if it's possible to process the...

    Read more »

  • I Got Bits, but...

    Jac Goudsmit05/31/2017 at 07:57 0 comments

    A quick update:

    I rewrote the code that regenerates the clock from the incoming SPDIF signal, and as you can see in the picture above (next to the RECCLK label), it's very steady now, and it runs at half the bit rate. So instead of generating one clock pulse (i.e. an up/down cycle) for each input bit, the RECCLK signal only changes once for each input bit.

    That means that the code in the data sampling cog(s) has to be repeated twice: first it waits for RECCLK to go high, then it reads a bit, then it waits for RECCLK to go low and reads another bit. This may look a bit sloppy but is not uncommon in Assembly programming: it's basically a partially unrolled loop. I may go one step further and unroll the entire loop for a single subframe (so the same code will be in the source file 32 times), if there's not enough time to get things done.

    The code uses a constant to delay the RECCLK signal by a variable number of clock cycles (it uses a timer in NCO mode). Basically:

    • The sync cog waits for the XORIN input (the "original" SPDIF xor'ed with the delayed SPDIF to detect flanks) to go high
    • It then starts a timer to toggle the RECCLK output
    • Then it waits until the output is actually toggled and starts the timer to toggle the RECCLK output again
    • After this, things start over from the top.

    I also added preamble detection code which correctly identifies preambles (see the PRADET trace) but needs a little work: I want to make it generate a single pulse that starts right when the first long input pulse is detected, and ends when the first bit of the next subframe comes in, but as you can see, it triggers multiple times. For now it's good enough to trigger my Logic Analyzer so I can record an entire subframe.

    I also implemented a small "debug monitor" cog that puts the data bits on a pin that's shown here as "DEBUG". As I mentioned in the previous log, the idea was to synchronize the data cogs on the recovered clock, and sample the XORIN pin at the time when RECCLK changes state. But this is actually a bad idea: The delay circuitry is very simple and there's a lot of jitter: I've seen recovered clock cycles that were more than 50ns too long or too short because of the jitter on the delay. That makes it pretty much impossible to get the timing right if I want to sample the signal right at the time when a second pulse comes in over XORIN.

    But the Propeller timers can be programmed to count negative or positive edges, so that the exact delay time in the external circuitry becomes a lot less critical. The Debug Monitor cog that I programmed, basically waits for the RECCLK signal to change, and then checks if the Propeller timer detected two negative edges in the last cycle. If so, it sets the DEBUG output to 1, otherwise it sets it to 0. As you can see, it correctly decoded the subframe in the middle (between the PRADET pulses) as (1)0000'0010'0110'1001'0001'0111'0001(0) (the 1 at the start and the 0 at the end are false readings because of the preamble, I'll deal with those later).

    What bothers me a bit though is that it was difficult to get the debug cog to work right: it's synchronized to the RECCLK with WAITPEQ/WAITPNE instructions but when I simply had the code read the data after each WAIT and then restart the timer, I couldn't get reliable results no matter how I set the delay for the sync cog. I had to basically:

    • Wait for the recovered clock RECCLK using WAITPEQ/WAITPNE
    • Wait for a little while longer using NOP
    • Check if there were 2 negative edges of XORIN
    • Reset the edge counter
    • Wait for the next edge of RECCLK.

    It bothers me that this way, the timing is partially based on the time it takes to execute the instructions. It means I'm doing something wrong or I need to use another timer in the debug cog (which may later become the data cog).

    I'll have to take a thorough look at the debug cog and analyze what's going on and where the delays are. For one thing, the timer that detects negative edges is delayed by one clock cycle.

    Another thing is that if I need...

    Read more »

  • Sending in the Big Guns

    Jac Goudsmit05/30/2017 at 08:44 0 comments

    As I noted in the previous log, S/PDIF is way too much to handle for one cog of a Propeller. That's okay, I have 7 more cogs. But in projects like this where you have to divide the work over several different cogs, it's difficult to keep your head around things. At the time that I'm typing this, the plan is somewhat as follows:

    • Unfortunately, some external circuitry is unavoidable. In the previous log I said I would like to get rid of it, but that's just impossible. The external circuitry converts the S/PDIF bi-phase signal of any polarity to a simple signal where each transition of the input is converted to a pulse. Then it's just a matter of timing the distances between those pulses to get the original binary stream.
    • One cog (let's call it the "sync cog") will be in charge of recovering the clock signal from the input, and will put that signal on a helper pin that can be used by other cogs for synchronization.
    • The same "sync cog" will also detect preambles and will generate output on another pin to synchronize the other cogs.
    • A "data cog" will use the output signals from the first cog to synchronize to the clock and read input bits. At the end of a subframe, it will store the 32-bit decoded word in the hub.
    • If I can't make the "data cog" fast enough to store data into the hub at the end of each subframe, there will be two "data cogs", one that reads the left subframe and one that reads the right subframe.
    • Once the data is in the hub, the subchannels can be demultiplexed and dumped to the serial port or to the screen. It will probably also be possible to use existing open-source modules to send the code out to a recorder or a playback device, after optionally modifying the data.

    I already noted that it was going to be necessary to mess around with the timers in the Propeller. Timers in the Propeller are incredibly useful -- as long as you just want to use them for output. For input tasks such as measuring time in a high-speed environment, the timers need a lot of help from the code:

    • You can't let the code wait for a timer (only for the system counter and it takes three assembly instructions to set that up, and that's two too many for our purposes)
    • There are timer modes that automatically start a timer when e.g. an input pin is low or high, to measure how long a timer input has been in that state. Unfortunately, the code still has to babysit the timer: if you're measuring how long a signal is high or low, the only way to tell that the signal is no longer in that state is to test it with assembly instructions (either by testing the signal directly or by testing if the timer is counting; both of which are several instructions that I don't have time for in this project)
    • There are 2 timers per cog which is really useful (16 timers total is not too shabby), but that means that one cog doesn't have direct access to a timer's state in another cog. So to synchronize one cog with the timer of another cog, you have to sacrifice a pin and use it for output on one cog and input on another cog.
    • There are some "logic" modes which allow you to analyze two input pins in various ways, but unfortunately it's not possible to generate direct output from these modes: the code has to examine the timer registers to see if anything has been registered. Nice for slow events but unusable for our purposes.
    • The timer modes that let you measure the time that an input pin was low or high, allow you to provide an output pin but the output is always just a delayed version of the input, and the delay is always simply the input pin delayed by one clock cycle.
    • There are various timer modes that generate useful output, especially the NCO and DUTY modes which use bit 31 of the counter, or the carry flag of an adder to generate output. This can be used to generate a pulse of a predefined length or after a predefined delay.

    When I started on the project, I had set up a simple circuit with a 74HC04 and some passive parts to amplify the input signal to the full range of CMOS, and...

    Read more »

View all 11 project logs

Enjoy this project?

Share

Discussions

Similar Projects

Does this project spark your interest?

Become a member to follow this project and never miss any updates