Wednesday 14 November 2018

Homebrew SSB Rig based on MC1350P IF Amp

See the YouTube channel for details:

https://www.youtube.com/channel/UCSNPW3_gzuMJcX_ErBZTv2g



Audio Amplifier



Note. Original schematic had a 47uF capacitor between the two stages. That is now a 3.3uF (as above). This reduces the quiet time when changing from TX to RX.


Antenna RF Amplifier




VFO/BFO



The Si5351 outputs go to two 1k ohm trim pots. Note, this is not the correct approach. A 50 ohm pad with the appropriate attenuation should be used. The YouTube videos explain why this is incorrect. The wiper arm goes to the two ADE-1 mixers. Both trim pots are set to approx. 1/3 from min. Adjust from min to get acceptable receive audio quality.  



************************************************************
This is the new code that works with Jason Mildrum's new Si5351 Etherkit library. See https://github.com/etherkit/Si5351Arduino




#include <Wire.h>
#include <SPI.h>
#include <LiquidCrystal_I2C.h>
#include <si5351.h>

const uint32_t bandStart = 7000000;     // start of 40m
const uint32_t bandEnd =   7300000;     // end of 40m
const uint32_t bandInit =  7100000;     // where to initially set the frequency
volatile long oldfreq = 0;
volatile long currentfreq = 0;
volatile int updatedisplay = 0;

volatile uint32_t freq = bandInit ;     // this is a variable (changes) - set it to the beginning of the band
volatile uint32_t radix = 1000;         // how much to change the frequency by, clicking the rotary encoder will change this.
volatile uint32_t oldradix = 1;

volatile uint32_t BFO_freq = 8003000;    // Crystal filter centre freq

// Rotary encoder pins and other inputs
static const int pushPin = 9;
static const int rotBPin = 2;
static const int rotAPin = 3;

// Rotary encoder variables, used by interrupt routines
volatile int rotState = 0;
volatile int rotAval = 1;
volatile int rotBval = 1;

// Instantiate the Objects
LiquidCrystal_I2C lcd(0x27, 16, 2);
Si5351 si5351;

void setup()
{
  // Set up frequency and radix switches
  pinMode(rotAPin, INPUT);
  pinMode(rotBPin, INPUT);
  pinMode(pushPin, INPUT);

  // Set up pull-up resistors on inputs
  digitalWrite(rotAPin, HIGH);
  digitalWrite(rotBPin, HIGH);
  digitalWrite(pushPin, HIGH);

  // Set up interrupt pins
  attachInterrupt(digitalPinToInterrupt(rotAPin), ISRrotAChange, CHANGE);
  attachInterrupt(digitalPinToInterrupt(rotBPin), ISRrotBChange, CHANGE);

  // Initialize the display
  lcd.begin();
  lcd.backlight();
  UpdateDisplay();
  delay(1000);

  // Initialize the DDS
  si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, 0);
  si5351.set_correction(31830, SI5351_PLL_INPUT_XO);      // Set to specific Si5351 calibration number
  si5351.set_pll(SI5351_PLL_FIXED, SI5351_PLLA);
  si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_8MA);
  si5351.drive_strength(SI5351_CLK2, SI5351_DRIVE_8MA);
  si5351.set_freq((freq * 100ULL), SI5351_CLK0);
  si5351.set_freq((BFO_freq * 100ULL), SI5351_CLK2);
}


void loop()
{
  currentfreq = getfreq();                // Interrupt safe method to get the current frequency

  if (currentfreq != oldfreq)
  {
    UpdateDisplay();
    SendFrequency();
    oldfreq = currentfreq;
  }

  if (digitalRead(pushPin) == LOW)
  {
    delay(10);
    while (digitalRead(pushPin) == LOW)
    {
      if (updatedisplay == 1)
      {
        UpdateDisplay();
        updatedisplay = 0;
      }
    }
    delay(50);
  }
}


long getfreq()
{
  long temp_freq;
  cli();
  temp_freq = freq;
  sei();
  return temp_freq;
}


// Interrupt routines
void ISRrotAChange()
{
  if (digitalRead(rotAPin))
  {
    rotAval = 1;
    UpdateRot();
  }
  else
  {
    rotAval = 0;
    UpdateRot();
  }
}


void ISRrotBChange()
{
  if (digitalRead(rotBPin))
  {
    rotBval = 1;
    UpdateRot();
  }
  else
  {
    rotBval = 0;
    UpdateRot();
  }
}


void UpdateRot()
{
  switch (rotState)
  {

    case 0:                                         // Idle state, look for direction
      if (!rotBval)
        rotState = 1;                               // CW 1
      if (!rotAval)
        rotState = 11;                              // CCW 1
      break;

    case 1:                                         // CW, wait for A low while B is low
      if (!rotBval)
      {
        if (!rotAval)
        {
          // either increment radixindex or freq
          if (digitalRead(pushPin) == LOW)
          {
            updatedisplay = 1;
            radix = radix / 10;
            if (radix < 1)
              radix = 100000;
          }
          else
          {
            freq = freq + radix;
            if (freq > bandEnd)
              freq = bandEnd;
          }
          rotState = 2;                             // CW 2
        }
      }
      else if (rotAval)
        rotState = 0;                             // It was just a glitch on B, go back to start
      break;

    case 2:                                         // CW, wait for B high
      if (rotBval)
        rotState = 3;                               // CW 3
      break;

    case 3:                                         // CW, wait for A high
      if (rotAval)
        rotState = 0;                               // back to idle (detent) state
      break;

    case 11:                                        // CCW, wait for B low while A is low
      if (!rotAval)
      {
        if (!rotBval)
        {
          // either decrement radixindex or freq
          if (digitalRead(pushPin) == LOW)
          {
            updatedisplay = 1;
            radix = radix * 10;
            if (radix == 100000)
              radix = 1;
          }
          else
          {
            freq = freq - radix;
            if (freq < bandStart)
              freq = bandStart;
          }
          rotState = 12;                            // CCW 2
        }
      }
      else if (rotBval)
        rotState = 0;                             // It was just a glitch on A, go back to start
      break;

    case 12:                                        // CCW, wait for A high
      if (rotAval)
        rotState = 13;                              // CCW 3
      break;

    case 13:                                        // CCW, wait for B high
      if (rotBval)
        rotState = 0;                               // back to idle (detent) state
      break;
  }
}


void UpdateDisplay()
{
  lcd.setCursor(0, 0);
  lcd.setCursor(0, 0);
  lcd.print(freq);
  lcd.setCursor(0, 1);
  lcd.print("ZL2CTM");

  if (radix != oldradix)                          // stops radix display flashing/blinking on freq change
  {
    lcd.setCursor(9, 0);
    lcd.print("       ");
    lcd.setCursor(9, 0);
    if (radix == 1)
      lcd.print("   1 Hz");
    if (radix == 10)
      lcd.print("  10 Hz");
    if (radix == 100)
      lcd.print(" 100 Hz");
    if (radix == 1000)
      lcd.print("  1 kHz");
    if (radix == 10000)
      lcd.print(" 10 kHz");
    if (radix == 100000)
      lcd.print("100 kHz");
    oldradix = radix;
  }
}


void SendFrequency()
{
  si5351.set_freq((freq * 100ULL), SI5351_CLK0);
  si5351.set_freq((BFO_freq * 100ULL), SI5351_CLK2);
}


************************************************************



BPF (note, 24g wire used, not 26g)


1st IF Amp


2nd IF Amp

Note. T1 secondary is now from Pin 4 and earth. Pin 6 is tied to earth via a 100nF cap.



Mic Amp (using an electret mic)


Note. Changed output coupling capacitor to a 1uF. Reduces short term CW on TX. A 100nF cap works well too.






Additional replay to allow the BPF and follow RF amplifier to be used for both RX (antenna amplifier) and TX (RF pre-amplifier)


RF Pre-Amp using NE592 Video Amp


Initial idea for the RF power amplifier. Based on two BLF1043 LDMOS devices.
Also using an old laptop power supply. 18.5VDC, 3A.
Please see the YouTube channel for test results! 








  



RF Driver Amplifier.



Current layout of the final radio


AGC. Designed to provide 6-7VDC to the MC1350P.



RC phase shift oscillator for antenna/amp tuning.



Final code supporting dual band function.


#include <Wire.h>
#include <SPI.h>
#include <LiquidCrystal_I2C.h>
#include <si5351.h>

const uint32_t bandStart80 = 3500000;     // start of 80m
const uint32_t bandStart40 = 7000000;     // start of 40m
const uint32_t bandEnd80 =   3900000;     // end of 80m
const uint32_t bandEnd40 =   7300000;     // end of 40m
const uint32_t bandInit =  3690000;       // where to initially set the frequency
volatile int band = 0;                    // 0 = 80m, 1 = 40m
volatile int oldband = 0;
volatile long oldfreq = 0;
volatile long currentfreq = 0;
volatile long freq80 = 3690000;
volatile long freq40 = 7120000;
volatile int updatedisplay = 0;
volatile long SMeterReadings[10];
volatile long SMeterAverage;
int AvCount = 0;

volatile uint32_t freq = bandInit ;     // this is a variable (changes) - set it to the beginning of the band
volatile uint32_t radix = 1000;         // how much to change the frequency by, clicking the rotary encoder will change this.
volatile uint32_t oldradix = 1;

volatile uint32_t BFO_freq = 9001350;

// Rotary encoder pins and other inputs
static const int pushPin = 9;
static const int rotBPin = 2;
static const int rotAPin = 3;
static const int bandSW = 4;

// Rotary encoder variables, used by interrupt routines
volatile int rotState = 0;
volatile int rotAval = 1;
volatile int rotBval = 1;

// Instantiate the Objects
LiquidCrystal_I2C lcd(0x27, 16, 2);
Si5351 si5351;

void setup()
{
  // Set up frequency and radix switches
  pinMode(rotAPin, INPUT);
  pinMode(rotAPin, INPUT_PULLUP);
  pinMode(rotBPin, INPUT);
  pinMode(rotBPin, INPUT_PULLUP);
  pinMode(pushPin, INPUT);
  pinMode(pushPin, INPUT_PULLUP);
  pinMode(bandSW, INPUT_PULLUP);
  pinMode(A7, INPUT);

  // Set up interrupt pins
  attachInterrupt(digitalPinToInterrupt(rotAPin), ISRrotAChange, CHANGE);
  attachInterrupt(digitalPinToInterrupt(rotBPin), ISRrotBChange, CHANGE);

  // Initialize the display
  lcd.begin();
  lcd.backlight();
  UpdateDisplay();
  delay(1000);

  // Initialize the DDS
  si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, 0);
  si5351.set_correction(31830, SI5351_PLL_INPUT_XO);      // Set to specific Si5351 calibration number
  si5351.set_pll(SI5351_PLL_FIXED, SI5351_PLLA);
  si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_8MA);
  si5351.drive_strength(SI5351_CLK2, SI5351_DRIVE_8MA);
  si5351.set_freq((freq * 100ULL), SI5351_CLK0);
  si5351.set_freq((BFO_freq * 100ULL), SI5351_CLK2);
}


void loop()
{
  band = digitalRead(bandSW);
  if (band != oldband)                      // Check if band has changed
  {
    if (band == 0)                          // Was 40m, now 80m
    {
      freq40 = freq;                        // Store 40m VFO freq
      freq = freq80;                        // Make VFO = stored 80m freq
    }
    if (band == 1)                          // Was 80m, now 40m
    {
      freq80 = freq;                        // Store 80m VFO freq
      freq = freq40;                        // Make VFO = stored 40m freq
    }
    UpdateDisplay();
    oldband = band;
  }

  currentfreq = getfreq();                  // Interrupt safe method to get the current frequency

  if (currentfreq != oldfreq)
  {
    UpdateDisplay();
    SendFrequency();
    oldfreq = currentfreq;
  }

  if (digitalRead(pushPin) == LOW)
  {
    delay(10);
    while (digitalRead(pushPin) == LOW)
    {
      if (updatedisplay == 1)
      {
        UpdateDisplay();
        updatedisplay = 0;
      }
    }
    delay(50);
  }

  SMeterReadings[AvCount] = analogRead(A7);
  AvCount++;
  if (AvCount == 10)
  {
    SMeterAverage = (SMeterReadings[0] + SMeterReadings[1] + SMeterReadings[2] + SMeterReadings[3] + SMeterReadings[4]
                     + SMeterReadings[5] + SMeterReadings[6] + SMeterReadings[7] + SMeterReadings[8] + SMeterReadings[9]) / 10;
    UpdateDisplay();
    AvCount = 0;
  }
}


long getfreq()
{
  long temp_freq;
  cli();
  temp_freq = freq;
  sei();
  return temp_freq;
}


// Interrupt routines
void ISRrotAChange()
{
  if (digitalRead(rotAPin))
  {
    rotAval = 1;
    UpdateRot();
  }
  else
  {
    rotAval = 0;
    UpdateRot();
  }
}


void ISRrotBChange()
{
  if (digitalRead(rotBPin))
  {
    rotBval = 1;
    UpdateRot();
  }
  else
  {
    rotBval = 0;
    UpdateRot();
  }
}


void UpdateRot()
{
  switch (rotState)
  {

    case 0:                                         // Idle state, look for direction
      if (!rotBval)
        rotState = 1;                               // CW 1
      if (!rotAval)
        rotState = 11;                              // CCW 1
      break;

    case 1:                                         // CW, wait for A low while B is low
      if (!rotBval)
      {
        if (!rotAval)
        {
          // either increment radixindex or freq
          if (digitalRead(pushPin) == LOW)
          {
            updatedisplay = 1;
            radix = radix * 10;
            if (radix > 100000)
              radix = 100000;
          }
          else
          {
            freq = freq + radix;
            if ((band == 0) && (freq > bandEnd80))
              freq = bandEnd80;
            if ((band == 1) && (freq > bandEnd40))
              freq = bandEnd40;
          }
          rotState = 2;                             // CW 2
        }
      }
      else if (rotAval)
        rotState = 0;                               // It was just a glitch on B, go back to start
      break;

    case 2:                                         // CW, wait for B high
      if (rotBval)
        rotState = 3;                               // CW 3
      break;

    case 3:                                         // CW, wait for A high
      if (rotAval)
        rotState = 0;                               // back to idle (detent) state
      break;

    case 11:                                        // CCW, wait for B low while A is low
      if (!rotAval)
      {
        if (!rotBval)
        {
          // either decrement radixindex or freq
          if (digitalRead(pushPin) == LOW)
          {
            updatedisplay = 1;
            radix = radix / 10;
            if (radix < 1)
              radix = 1;
          }
          else
          {
            freq = freq - radix;
            if ((band == 0) && (freq < bandStart80))
              freq = bandStart80;
            if ((band == 1) && (freq < bandStart40))
              freq = bandStart40;
          }
          rotState = 12;                            // CCW 2
        }
      }
      else if (rotBval)
        rotState = 0;                               // It was just a glitch on A, go back to start
      break;

    case 12:                                        // CCW, wait for A high
      if (rotAval)
        rotState = 13;                              // CCW 3
      break;

    case 13:                                        // CCW, wait for B high
      if (rotBval)
        rotState = 0;                               // back to idle (detent) state
      break;
  }
}


void UpdateDisplay()
{
  lcd.setCursor(0, 0);
  lcd.setCursor(0, 0);
  lcd.print(freq);
  lcd.setCursor(0, 1);
  lcd.print("ZL2CTM");

  lcd.setCursor(11, 1);
  if (SMeterAverage <= 850)
    lcd.print("S3 ");
  if ((SMeterAverage > 851) && (SMeterAverage <= 853))
    lcd.print("S4 ");
  if ((SMeterAverage > 854) && (SMeterAverage <= 864))
    lcd.print("S5 ");
  if ((SMeterAverage > 865) && (SMeterAverage <= 904))
    lcd.print("S6 ");
  if ((SMeterAverage > 905) && (SMeterAverage <= 989))
    lcd.print("S7 ");
  if ((SMeterAverage > 990) && (SMeterAverage <= 1010))
    lcd.print("S8 ");
  if (SMeterAverage > 1011)
    lcd.print("S9 ");

  if (radix != oldradix)                          // stops radix display flashing/blinking on freq change
  {
    lcd.setCursor(9, 0);
    lcd.print("       ");
    lcd.setCursor(9, 0);
    if (radix == 1)
      lcd.print("   1 Hz");
    if (radix == 10)
      lcd.print("  10 Hz");
    if (radix == 100)
      lcd.print(" 100 Hz");
    if (radix == 1000)
      lcd.print("  1 kHz");
    if (radix == 10000)
      lcd.print(" 10 kHz");
    if (radix == 100000)
      lcd.print("100 kHz");
    oldradix = radix;
  }
}


void SendFrequency()
{
  si5351.set_freq(((BFO_freq - freq) * 100ULL), SI5351_CLK0);
  si5351.set_freq((BFO_freq * 100ULL), SI5351_CLK2);
}

15 comments:

  1. Hi Charlie, I just finished building your amp this evening onto a breadboard. It works great! I substituted a LM380N-8 because that was all I had in my junk box. I also used my old Elenco Function Generator to provide a 1kz sine wave into the amp's input. Your amp has plenty of volume and I had to crank it way down not to hurt my ear! Thanks for sharing your circuit and design methodology. I will build the rest of the rig along side of you. 73 de Orville

    ReplyDelete
  2. That's great Alan. It's a simple audio amp, but works really well. Just make sure you have a trim pot for Rf so you can tweak it to suit the final receiver gain. I've just finished the RF amp and that's working great too. I'll put a video up on that today.

    Charlie

    ReplyDelete
  3. Took me some time to dig this up, check it, and clean the archive. It is a collection of articles by John Pivnichny N2DCH. He used the MC1350 a few times with single ended in/outputs. Tossed in a few articles on xtal filters (he wrote a book), a couple of linear display analog VFOs, and a couple of test gear (signal source, dbm meter, attenuators).
    https://bit.ly/2DVX1VB

    Thanks again for sharing.

    ReplyDelete
    Replies
    1. Excellent. Thanks Michael. Looks like some really interesting info in that. Thanks again.

      Charlie

      Delete
  4. If you were looking for square wave at the drain for switching efficiency then the output of the driver chip should be okay. I see it looking at about 250 ohm at the LDMOS gate. To my way of thinking the load would be 11pf at signal voltage and 9 pf at the sum of the signal voltage and the change in the drain voltage. Maybe 90 pf equivalent. That's about 100 pf at 7 Mhz=270 ohms. If the LDMOS drive voltage needs to be about 5 V then and the chip outputs 10V then a 2:1 transformer. Then the load on the chip would be 2 channels * 100 mW. If it can handle a bit more then direct connection would be good.
    That is all guessing you are not trying for class A. Anyway 1 ohm input impedance seems to me way off.

    ReplyDelete
    Replies
    1. Thanks Sean. I'm going to try some more experiments with a higher Vcc and different quiescent current level. I also know the output transformer is incorrect. I'll also look at potentially driving the LDMOS' directly from the driver as you suggest. Hopefully, no magic smoke will be released!

      Delete
  5. I looked at your equations and the circuit for the 2N3904 rf amp and thought of rearranging things a bit:
    Replace the transformer with a bifilar wound one, windings connected in series. The output is capacitor coupled from the center tap, which provides the necessary 4:1 impedance transformation.
    The feedback resistor is connected in series with the 6.8k bias resistor and now goes to the transformer center tap. I think this resistor now needs to be 125 ohms (would use 150). The 6.8K resistor is shunted with a 100nf capacitor.
    With this arrangement we could switch around the amp with a DPDT relay. The two 'arms' of the relay are input and output via 100nf caps to the rest of the circuit. The 6.8K in parallel with the 100nf cap, both in series with the 150 ohm feedback resistor are connected to the relay switch arms. The NO contacts go to the base and transformer output connections. The NC contacts are shorted. Now the relay when energized the amp is powered (biased on) is in circuit. With the relay unenergized the amp is bypassed.

    ReplyDelete
  6. Thanks for that, I'll take a look. I'm starting to look more closely at feedback design to increase bandwidth. The hard part is trying to qualitatively determine what impact that has on input and output impedance. Thanks again for the feedback.

    ReplyDelete
  7. Hi Charlie, I am a passionate radio builder in the field in Romania, I watched with great care the videos posted on youtube, as well as the constructive details on the blog. I am very interested in building such a RIG. Unfortunately, the SSB filter I have in my hand is retrieved from a Kenwood TS-120S with an 8.83 MHz frequency impedance of 600 ohms with 15pF. If you can help me with some details on the attenuator value from the output of the IF amplifier and the adaptor transformer from the input of the SSB filter and if at the input of the IF amplifier with the MC1350P still has an adaptation transformer if I use the Kenwood filter. Thanks in advance, Happy Holidays.
    73! by YO5PQJ, Alex

    ReplyDelete
  8. Hi Charlie, shouln't AvCount be set back to zero once it reaches 10? It's currently being reset to one.

    Happy new year!

    Cheers, Ian

    ReplyDelete
    Replies
    1. Hi Ian. Yes you are absolutely correct. Well spotted. The code above has been updated.

      Thanks again.

      Charlie

      Delete
  9. Hi Charlie,
    Is the script for your MC1350 SSB rig on BitHub? Or, do I need to just copy it and paste into the IDE editor? I tried the last and some errors showed up that I don't really understand.

    Thanks, Jack

    ReplyDelete
  10. This comment has been removed by a blog administrator.

    ReplyDelete
  11. no matching function for call to 'LiquidCrystal_I2C::begin()
    how fix this err

    ReplyDelete
    Replies
    1. I can't help from here sorry. Find a library that works with your display. Use the library examples to test this. Once it is going, copy across the syntax into the radio sketch.

      Delete

Note: only a member of this blog may post a comment.