Overview

At the beginning the antenna is directly connected to the AD9226 Analog to digital converter, sampling at 64MHz. The 64MHz clock is generated by the FPGA, so that it can sample it well (more on this later)

Please note there's no external PC because the Zynq 7010 embeds a Dual Core Cortex A9 running Linux (Xilinx PetaLinux) in addition to its powerful FPGA.

Similar projects

KiwiSDR is a very good example of a wideband 0-30 MHz FPGA based SDR receiver but it costs 450 Euro and it needs a BeagleBone computer. 

Red Pitaya is a wonderful project with a lot of possibilities and it is open source but the board costs around 600 Euro.

Panoradio (https://panoradio-sdr.de/panoradio-sdr/) is much more than I'm trying to build (100 MHz spectrum, 16 bits samples) but its components (FPGA board, A/D board) are much more expensive as well. 

Block Diagram


At this time (Aug 2023) the whole design is pretty consolidated.

TODO:

Numbers

All the number crunching in the fabric logic is perfomed with two's complement. 

When I write xx(yy), e.g. 32(28), I mean that the numbers are 32 bit two's complement but the maximum expected value is 

                                                  - 2^27 <= value <= 2^27-1

ADC - Analog to Digital Converter - AD9226

The board has no clock reference (see the AD9226 board schematic) and therefore the 64MHz clock must be supplied by the FPGA. In this way:

See the picture below and for further details see the AD9226 datasheet from Analog Devices.

A concern is about the clock jitter coming from the FPGA. Will it add noise to the received signal?

For example, in this project ADC to DMA to Ethernet with a ZYNQ 7000, the sampling clock is generated outside the FPGA 

FPGA

The functions operating in the FPGA are the following.

Getting started with the EBAZ4205 board

The whole project is quite challenging, especially for those people who have never worked with Xilinx Zynq development environment (Vivado, Vitis, Petalinux). Therefore I strongly suggest to start with a "getting started" project.  See my Hackaday project: EBAZ4205 development environment

The Analog Input

I didn't like that AD9226 12-BIT 65MSPS  board attenuates the input signal by -8,4 dB. So I decided to modify it to get some amplification, using far more 12 bits ADC range. 

I designed a circuit like this: 

obtaining some 16,5 dB gain compared with -8,4 dB of the original circuit that was this one:

The modified board:

The Analog Input Dynamic Range

With the first stage amplification I set, the maximum input signal (the one generating 2 Vpp between VINA and VINB) is around -6 dBm.

Ethernet

The Ethernet port is connected to the local LAN, obtaining an IP address.

In this way, using an external PC, you can:

GUI

I decided to embed everything in the Zynq-7010, so I built a QT5 C++ application to view the spectrum and tune the radio stations. Its GUI appears like in the following pictures.

In this example, a 100 KHz wide spectrum where a radio station at 13,67 MHZ is tuned (see the vertical green line). Please note also the mouse point in red and the available settings on the right.

A 32 MHz wide spectrum where a radio station at 13,67 MHZ is tuned (the same as above).

RECEIVE SOME RADIO STATIONS AND HAMS

The solution implemented to display the spectrum by capturing sampled input burst can't obviously be used to receive radio stations, because:

Therefore I designed:

to listen to some audio at 48 Ksamples / second after getting it out the Zynq-7000 from.

32 MHz Low Pass Filter 

(TODO)

 I need to attenuate the strong FM radios (88-108MHz) entering in the spectrum by the 3rd and 4th Nyquist zone and also some commercial repeater in the 2nd Nyquist zone. Hey! Don't you know much about Nyquist zones? Urgently read this: Nyquist Criterion and Sampled Data System Design.

According to crun suggestions I should filter away the FM radio band (88-108) and also the AM band (up to 1,6 MHz). To filter the FM radios I thought to use an 9th order elliptic filter like this.

But I didn't do anything at the moment, because:

DDC (Digital Down Converter)

As usual, It is composed by a Local Oscillator and a Complex Multiplier.

Note that the Local Oscillator, its AXI Control and the Complex Mutliplier all use a 64 MHz clock (CLK).

Local Oscillator

Complex Multiplier

Test Signal Generator

To test the fabric logic and the whole design even without an external signal and without involving the ADC, I rapidly understood the need of an internal test signal. Using a Xilinx DDS compiler IP I designed the following block:

It produces a carrier AM modulated with 1 KHz tone. The carrier frequency can be set from 0 to 32 MHz (and beyond in necessary).

CIC-FIR 

I found two very well explained articles: 

Main Facts about CIC-FIR

The standard configuration is to design one or two programmable consecutive CIC filters/decimators, followed by a fixed FIR filter. 

about CIC:

about FIR:

While testing my CIC-FIR dual channel solution I met a problem I submitted to the Xilinx/AMD support.

AM Demodulator

I essentially copied the design by K6JCA - An FPGA SDR HF Transceiver

AM demodulator

It is a classical envelope detector performing sqrt ( I^2 + Q^2 ) . 

See below in yellow a test signal (the I component only) entering this AM demodulator and in blue the AM demodulated.

No DC and signal level meter

To remove the DC component from the AM demodulated, I simply used a first order IIR highpass filter, which can also give the carrier level obtained low passing the AM demodulated signal.

See below in red the AM demodulated and with the DC component removed by the high pass IIR filter. In green the carrier level.

This is the complete testbench I used in Simulink.

Beside the AM demodulator and the high/low pass filter, note the test signal I used (leftmost block). It is designed to produce a 1 second AM modulated signal followed by 1 second without any signal, to test the transient response of the signal level meter.

This is its block diagram 

and its output (I component only) in yellow.

In green you can see the signal level meter response.

AGC

Designed and simulated but not tested yet

It acts after AM / SSB demodulation.

It is the kind of Automatic Gain Control called "forward" i.e. it adjusts the output level without reaction on previous stages but simply amplifying / attenuating the signal itself.

Simulated with Simulink and converted to VHDL.

I2S Test

I decided to use I2S to get audio out from the Zynq. To better understand the reasons, see the log.

To test the IP (I2S_Transmitter) and the I2S DAC (PCM5102) I built this Vivado block:

A few facts:

This is the I2S data protocol:

BCLK is SCLK       SDATA is I2SDATA           fs = 48.000 Hz

DAC (PCM5102) and class D audio amplifier (PAM8403) schematic.

Please note:

And finally these are the left and right outputs. 

WSJT-X / JTDX

It runs inside the Zynq PS as a normal Linux program to decode FT8 or other digital mode. See the GitHub project for details.

Of course the JTDX program must be supplied with an audio input. See the next paragraph.

Audio output (from PL to PS)

As you can see in the block diagram, the output of the I2S transmitter goes out to the I2S (DAC) decoder but it also goes to a "I2S to DMA" block by which the audio samples are copied to a memory mapped area via DMA and collected from the "Xilinx I2S audio Linux driver".

Zoom FFT Receiver

I copied the idea from Panoradio (https://panoradio-sdr.de/panoradio-sdr/).

In fact if you need to view the spectrum details of modulated signals, you need to zoom the spectrum collecting a sub band of it. 

E.g. 8192 time samples of a 0-32MHz spectrum let's you to view the spectrum with a "definition" (frequency bin span) of 32M/8192 = 3906,25 Hz which means you see the full FT8 7,074 MHz band in a single bin  . 

For this reason I built such a separate receiver (DDC + IF Filter + AXI Capture) to get the RF spectrum of a desired sub band with the wanted definition. See Spectrum DDC (Zoom FFT) in the block diagram at the beginning of these "details".

SSB Demodulator

The obvious choice is to build a Weaver demodulator, very well described here Weaver SSB Modulation/Demodulation - A Tutorial by Derek Rowell.

You should also read the original article by Donald Weaver A Third Method of Generation and Detection of Single-Sideband Signals," Weaver, Proceedings of the IRE, Dec., 1956  

This is my design.

Of course the frequency of the BFO must be set at the boundary of the IF low pass filter, as described in the article.

Summary:
To demodulate an USB signal with frequency ωc (carrier frequency) using a low-pass filter
with cut-off frequency ωo,


To demodulate an LSB signal with frequency ωc (carrier frequency) using a low-pass filter with cut-off frequency ωo, 

where:

My Assembly

I placed all the boards in a plastic box scavenged from an old modem.


Even the HDMI screen (Samsung 1366 x 768 LCD) was scavenged from an old HP laptop and adapted to HDMI using an HDMI to LVDS interface board (10 $)

FT8 QSO's

Now that the whole project works pretty well as a receiver, including FT8 decoding, it's time to transmit anything. To know something about the FT8 protocol, see FT4_FT8 and my resume in the log. 

MYJTDX software (forked from JTDX forked from WSJT-X) decodes well and it is ready to transmit and manage QSO's. 

I decided (see the log for details) to use an external DDS, programming its output frequency to:

See the AD9851 DDS datasheet and the  AD9851 (DDS + DAC) board I used.

Zynq-7010 - AD9851 Wirings

Note the RC low pass filter (R1-C1) to get a DC level from the PWM 2 MHz waveform. If it is not enough attenuating 2 MHz, I'll change it with an LC multiple poles low pass filter.

FT8 Gaussian Frequency Shift Keying Transmitter

My second attempt to get the FT8 messages out from the myjtdx linux app and send them to the external DDS (AD9851 board) succeeded!

For those of you interested in undestanding the first unsuccessful attempt, it is documented in this log Vivado AD9851 first attempt.

For this final solution a home made IP component written in VHDL (PL GFSK in the following picture) receives via AXI4-Lite the 79 symbols (each symbol is 3 bits each) composing the FT8 message. In this way the Linux software (MYJTDX) and the Cortex A9 CPU have plenty of time to send these 10 x 32 bits every 15 seconds. Besides, the carrier frequency (e.g. 7075500 Hz) must be sent to the AD9851 because the user can choose it by the MYJTDX user interface..

Therefore the message from PS to PL is composed by the following 11  x 32 bits words.

                 MSB                                                                        LSB             


NOTE Please note that an FT8 symbol (0...7) could be encoded with 3 bits but for AXI4-Lite convenience (8 bits alignment) I decided to encode a symbol in 4 bits leaving the MSB at 0.

As I already built the AD9851 PWM and AD9851 serializer, it's now time to design the GFSK IP.

FT8 Ramp up and down - VHDL implementation

According to the FT8 protocol, the first and last symbols amplitude shall follow a ramp up and a ramp down.

The IP component I  designed generates a DC signal (averaging a 2MHz PWM) to amplitude modulate the AD9851.

For details about AD9851 Amplitude Modulation see this

See the result for 20 miliseconds ramp down in the following picture, where you can see the blue voltage:

and the corresponding signal (DAC output) amplitude in yellow.

The raised cosine ramp down values are obtained by the following array of constant values


constant rampup     : rampup_type := (0,0,0,0,0,2,14,37,70,108); -- 20 msecs ramp up    

used in this way:

FT8 Samples and Symbols - VHDL impementation

When the 11 registers described in "FT8 Gaussian Frequency Shift Keying Transmitter" are loaded, the VHDL code start counting the 81 symbols (sym_counter increases every 160 milliseconds) and, in an inner loop, 80 samples (sample counter increases every 2 milliseconds).

See the following main process which sets the values of:

every 2 milliseconds

process( read_data_out_t ) is
begin
  -- 149 is 6.25Hz * 2^32 /180MHz
  -- phi_out <=  std_logic_vector ( b"00000001" & to_unsigned(  to_integer(unsigned(phi0)) +  149 * to_integer(unsigned(data_out_t)) / 1024, 32 ));
  phi_out <=  std_logic_vector ( unsigned(ad9851_ctrl) & to_unsigned(  to_integer(unsigned(phi0)) +  149 * to_integer(unsigned(data_out_t)) / 1024, 32 ));
  -- data_out <= data_out_t;
  if (rising_edge (read_data_out_t)) then
    if ( S_AXI_ARESETN = '0' ) then
       sym_counter <= 0;
       sample_counter <= 0;
       pwm_dc <= x"FF";
       last_msg <= msg_counter;               -- this msg is completely sent to AD9851
    elsif  sample_counter < 79 then           -- 80 samples per symbol
       sample_counter <= sample_counter + 1;
           
       -------------------------------------------------------------------------------------------------
       -- build the values of data out_t and pwm_dc for sym_counter (0,80) and sample_counter(0,79)
       if(sym_counter = 0) then
           if(sample_counter < 10) then
               ad9851_ctrl <= b"00000001"; -- AD9851 power on 
               pwm_dc <= std_logic_vector( to_UNSIGNED((255-rampup(sample_counter))*pwm_divider/255,8));  -- first half ramp from 0 to 102
               data_out_t <= std_logic_vector( UNSIGNED(symbols(0)) * TO_UNSIGNED(1024 , 29));
           elsif(sample_counter < 20) then
               pwm_dc <= std_logic_vector( TO_UNSIGNED ((rampup(19-sample_counter))*pwm_divider/255,8)); -- second half ramp from 154 (256-102) to 255 (256-0) 
               data_out_t <= std_logic_vector( UNSIGNED(symbols(0)) * TO_UNSIGNED(1024 , 29));
           else 
               data_out_t <= std_logic_vector( UNSIGNED(symbols(0)) * TO_UNSIGNED(1024 , 29));
           end if;
       elsif(sym_counter < 80) then
           -- sym_counter = 1 ... 79
           if(sample_counter < 19) then
               data_out_t <= std_logic_vector( 
                       UNSIGNED(symbols(sym_counter-1)) * TO_UNSIGNED(1024-pulse(18-sample_counter), 29) + 
                           UNSIGNED(symbols(sym_counter  )) * TO_UNSIGNED(pulse(18-sample_counter)     , 29)  
               );
           elsif sample_counter<60 then
               --- 19 <= sample_counter < 60
               data_out_t <= std_logic_vector( 
                           UNSIGNED(symbols(sym_counter  )) * TO_UNSIGNED(1024                     , 29)  
               );
           else
               --- 60 <= sample_counter < 80
               data_out_t <= std_logic_vector( 
                       UNSIGNED(symbols(sym_counter  )) * TO_UNSIGNED(pulse(sample_counter-60)     , 29) + 
                       UNSIGNED(symbols(sym_counter+1)) * TO_UNSIGNED(1024-pulse(sample_counter-60), 29)  
               );
           end if;
       else 
           -- sym_counter = 80
           data_out_t <= std_logic_vector( UNSIGNED(symbols(80)) * TO_UNSIGNED(1024 , 29));
              
       if(sample_counter >= 60 and sample_counter < 70) then 
           -- ramp down first half
           pwm_dc <= std_logic_vector( to_UNSIGNED(rampup(sample_counter-60)*pwm_divider/255,8));  -- first half ramp from 0 to 102
       elsif(sample_counter >= 70 and sample_counter < 80) then
           -- ramp down second half
           pwm_dc <= std_logic_vector( TO_UNSIGNED ((255-rampup(79-sample_counter))*pwm_divider/255,8)); -- second half ramp from 154 (256-102) to 255 (256-0) 
       end if;
        end if;
    -- end of build the value of data out for sym_counter (0,80) and sample_counter(0,79)
    -------------------------------------------------------------------------------------------------
    else   
        -- sample counter reached 80 -> reset it to 0
        sample_counter <= 0;
        if sym_counter = 80 then
             sym_counter <= 0;
             last_msg <= msg_counter; -- this msg is completely sent to AD9851
             ad9851_ctrl <= b"00000101"; -- AD9851 power off 
               
        else
             sym_counter <= sym_counter + 1;          
        end if;       
           
    end if;  -- end if sample_counter
  end if;    -- end if rising edge
end process;

 You can see the full VHDL source code of AD9851_gfsk on my Github repository.