The I2S bus was introduced by Philips Semiconductor in 1986. Its purpose is to provide a simple serial interface capable of transferring digital audio data between integrated circuits so, when I needed a way to transfer data between a Teensy microcontroller and my Papilio One FPGA board, an I2S receiver was a natural choice.
I like to work with the ZPUino System-on-Chip (SoC) so I prefer FPGA blocks containing a WishBone interface. Since the I2S signal specification isn’t that complicated I decided to write one myself.
Examining the I2S Data Stream
The figure below from the I2S bus specification shows a single frame in an I2S data stream.
There are three signals, the serial clock (SD), left-right clock (WS), and serial data (SD). The serial clock and left-right clock can be generated from either the transmit or receive side. The side that generates them is called the master, the side receiving them is the slave.
For the case I’m interested in (transferring digital audio from a Teensy microcontroller to an FPGA), the clocks are generated by the Teensy so the block described here is an I2S slave receiver. Implementing it comes down to three things; clock synchronization/edge detection, left-right channel detection, and sampling the audio data.
Clock Synchronization and Edge Detection – FPGAs are synchronous devices. They like to operate on the rising edge of their clocks. Unfortunately an I2S slave receiver operates within the transmitter’s clock domain so the first thing that needs to be done is bring the I2S data stream into the FPGA clock domain.
A circuit for doing this is shown below. The first flip-flop samples the incoming data stream. There will be meta-stability problems with the signal out of the flop-flop because we’re dealing with two different clock domains and a violation of the flip-flop setup and hold time at some point is inevitable. Therefore, the output of the first flip-flop is sampled by a second flip-flop. The output of the second flip-flop puts the incoming data stream into the FPGA clock domain.
But as mentioned above, FPGAs like to operate on system clock edges. To identify the clock edges in the incoming data stream a third flip-flop is added. The clock edges are recognized by looking for differences in the output of the second and third flip-flops. VHDL code to do this for the I2S serial clock is listed below. Code for the left-right clock is similar.
-- Process to detect the positive and negative edges of the sclk. -- This process syncs the sclk into the FPGA clock domain. -- For a description of how this works, see -- https://www.doulos.com/knowhow/fpga/synchronisation/ -- These edge signals are only valid for a single system clock. -- detect_sclk_edge : process(wb_rst_i, wb_clk_i) begin if wb_rst_i = '1' then zsclk <= '0'; zzsclk <= '0'; zzzsclk <= '0'; sclk_pos_edge <= '0'; sclk_neg_edge <= '0'; elsif rising_edge(wb_clk_i) then zsclk <= sclk_in; zzsclk <= zsclk; zzzsclk <= zzsclk; if zzsclk = '1' and zzzsclk = '0' then sclk_pos_edge <= '1'; sclk_neg_edge <= '0'; elsif zzsclk = '0' and zzzsclk = '1' then sclk_pos_edge <= '0'; sclk_neg_edge <= '1'; else sclk_pos_edge <= '0'; sclk_neg_edge <= '0'; end if; end if; end process;
Left-Right Channel Detection – Once the code to locate rising and falling edges of the left-right clock is implemented, recognizing the start of the left and right channel data is as simple as looking for left-right clock falling and rising edges.
Recognizing the start of the left and right channel data is important since that’s when initialization required to sample the data for that channel is done. Examples include initialization of the channel data register, clearing the counter used to keep track of sampled data bits, etc. Once the initialization is complete you can move on to …
Sampling The Audio Data – While the left-right clock edge occurs on a falling edge of the serial clock, the serial audio data is valid and sampled on the rising edge. It begins on the second serial clock rising edge after the left-right clock edge and is sent most significant bit (MSB) first. Sampling the audio data is as simple as shifting the appropriate number of bits into registers for the left and right channels. Code to accomplish the left-right channel detection and data sampling is listed below.
-- -- Process to create state counter for the left-right -- channel. -- process(wb_rst_i, wb_clk_i) variable left_state : integer := 0; variable right_state : integer := 0; begin if wb_rst_i = '1' then -- -- Initialize. -- left_channel_state <= (others => '0'); left_channel_data <= (others => '0'); left_channel_reg <= (others => '0'); right_channel_state <= (others => '0'); right_channel_data <= (others => '0'); right_channel_reg <= (others => '0'); sample_ready <= '0'; elsif rising_edge(wb_clk_i) then -- -- Prepare to receive channel data on the rising/falling edge of the -- left/right clock. -- if lrclk_neg_edge = '1' then -- -- Starting the left channel so initialize and save the right channel. -- left_channel_data <= (others => '0'); left_channel_state <= (others => '0'); case control_reg(0) is when '0' => -- -- I2S format. -- right_channel_reg <= right_channel_data(AUDIO_DATA_WIDTH-2 downto 0) & '0'; when '1' => -- -- Left justified. -- right_channel_reg <= right_channel_data(AUDIO_DATA_WIDTH-1 downto 0); when others => end case; -- -- Having saved the right channel sample indicates the end of an audio -- frame. Set the flag indicating the sample pair is ready. -- sample_ready <= not(sample_read); elsif lrclk_pos_edge = '1' then -- -- Starting the right channel so initialize and save the left channel. -- right_channel_data <= (others => '0'); right_channel_state <= (others => '0'); case control_reg(0) is when '0' => -- -- I2S format. -- left_channel_reg <= left_channel_data(AUDIO_DATA_WIDTH-2 downto 0) & '0'; when '1' => -- -- Left justified. -- left_channel_reg <= left_channel_data(AUDIO_DATA_WIDTH-1 downto 0); when others => end case; end if; -- -- Process incoming data on the rising edge of the sclk. -- if sclk_pos_edge = '1' then -- -- Processing for the left channel. -- if left_channel_clock = '1' then -- -- Update the left channel state -- left_state := to_integer(left_channel_state); left_channel_state <= to_unsigned(left_state + 1, CHANNEL_STATE_LENGTH); -- -- Update the channel and output data. -- if left_state < AUDIO_DATA_WIDTH then left_channel_data <= left_channel_data(AUDIO_DATA_WIDTH-2 downto 0) & audio_in; end if; else -- -- Update the right channel state -- right_state := to_integer(right_channel_state); right_channel_state <= to_unsigned(right_state + 1, CHANNEL_STATE_LENGTH); -- -- Update the channel and output data. -- if right_state < AUDIO_DATA_WIDTH then right_channel_data <= right_channel_data(AUDIO_DATA_WIDTH-2 downto 0) & audio_in; end if; end if; end if; end if; end process;
Using the Block
The VHDL source along with a ZPUino class for using it is available in my GitHub repository. Control and status registers for the Wishbone interface are as defined below.
|IN||000||Control Bits||0 – Left/Right Align2|
|OUT||000||Status Bits||0 – Left/Right Align current value
1 – Audio sample ready
|001||Audio Sample||31 bit sample. Upper 16 bits contain the left channel data. Lower 16 bits contain the right channel data.||
1. Status and control bits are active high.
2. If 0 the audio data within a channel is sampled beginning on the second rising edge of the serial data clock. If 1 sampling begins on the first rising edge.
The block was tested at 44.1 kHz but there’s nothing rate specific so it should function just fine at 48 kHz.
This writeup isn’t a new project. It’s more of a gathering of disparate pieces of information into one place.
For a while now I’ve been looking to do some more advanced RF design and testing using my HackRF One but realized the stock crystal oscillator wasn’t going to cut it. But this is an already solved problem. High accuracy TCXOs are widely available for the HackRF. But finding the information necessary to take advantage of them took a little more work. You’ll not only need the TCXO but a bigger case as well.
Installing The TCXO
To install it, take a look at this photo from the gps-sdr-sim repository. It clearly shows where the TCXO gets installed in the HackRF. Just take the HackRF out of its case and insert the TCXO as shown. To test it, clone the HackRF tools repository and build them. Plug in the HackRF and execute the command:
hackrf_si5351c -n 0 -r
If you get the response
[ 0] -> 0x01
the HackRF has recognized and is using the TCXO.
You’re Going To Need A Bigger Case
You’re going to find that the HackRF and TCXO are too tall to fit inside the standard case. One solution is to cut a hole in the top as shown in this thread. But once you do that there’s no going back so I decided to look around for alternatives.
Brian Dorey has a good discussion of how to install the HackRF into a Hammond 1455J1201 case but the case end templates he provides assume you’ll be mounting the HackRF on the case center rails. The HackRF and TCXO are too tall for this position so instead I 3D printed a couple of case ends that allow you to mount the HackRF on the case lower rails. This provides enough clearance for the HackRF and TCXO. The stl files for these case ends are available here.
And Now It’s Done
Photos of the complete project are shown below. I’m really happy with the way it turned out; the black case ends fit right in. Their only shortcoming is the LEDs and connectors aren’t labeled like on the standard case but I can live with that, and now the real fun can begin.
With the increased capabilities of laptops, desktop computers, and even embedded systems, amateur digital modes are typically generated via software. This includes modes like WSPR and JT-65 as well as classic modes like RTTY.
For the last couple of months I’ve been working on a low-power transmitter intended to be used with these modes. At its most basic the problem is a simple one. The digital signal is generated at audio frequencies and the transmitter simply needs to translate that signal to the desired operating frequency.
In practice, the problem is a little more complicated. I considered a number of different ways to accomplish it but ultimately fell back on an old standard, a phasing style SSB transmitter. While it’s still a work in progress, I’ve gotten far enough along that I thought it was time I started documenting it.
The approach I decided upon is split into two parts; signal generation and frequency translation. My intention is to have the baseband audio signal generated in software using one of the generally available packages that produce a dual-channel I/Q audio stream. This simplifies the hardware design by eliminating the need for the phase shift network normally found in phasing style SSB transmitters.
The hardware for frequency translation is based upon W1TAG’s Low Frequency Phasing Exciter. The first time I saw this method used was in a September 1990 article by PA0DEN (van Graas, 1990), who referred to it as “The Fourth Method” of generating SSB signals. W1TAG’s design was originally intended for use in the LF bands but it wasn’t difficult to update for use in the HF bands.
I started by replacing the 4 4066 SPST FET switches in the original design with a single CBT3253 4-to-1 multiplexer. I also replaced the external oscillator and ring counter used to generate the switch waveforms with a single Si5351 that has clocks 0 and 1 configured to operate at the same frequency but 90o out of phase. Since I intend for this hardware to only function as a SSB exciter I eliminated the transistor output amplifier. Finally, I replaced the TL072/074 op-amps with AD8051/8052 capable of operating at HF. The updated circuit is shown in the figures below.
You may notice there is no controller shown for the Si5351. I intend to include one eventually but for now I’m just using the Si5351 USB controller I described in one of my previous posts.
Testing The Hardware
For initial testing I used G3PLX’s software IQ transmitter. The Si5351 was tuned to a convenient frequency. The software’s Transmit mode was set to USB and modulation to 1 kHz tone as shown below. These settings produced a 1 kHz tone of approximately 2V peak-to-peak at the soundcard output.
With the soundcard connected to the transmitter input I could hear (and see, using HDSDR) the expected tone approximately 1 kHz above the Si5351’s tuned frequency.
Looks Good So Far
So that’s where things stand. It’s by no means ready for prime time. I’m getting about 20 mW into a 50 ohm load. Even at that low level, without a filter on the output I wouldn’t transmit anywhere but into a dummy load and I’m on the lookout for a good metal case to provide some shielding. Even so, it’s a nice start and I’ll post updates as it progresses.
Van Graas, D. H. “The Fourth Method: Generating and Detecting SSB Signals.” QEX (Sept 1990): 7-11.