A Half-baked Fix for Si5351 Quadrature Issues
In a previous post, I briefly described issues I had preserving quadrature output from an Si5351 board. Allegedly, once a 5351 is programmed to provide a fixed phase offset on a second output, it should maintain that phase shift across frequency excursions, provided the "even divisor" remained unchanged - No PLL reset needed until the a large enough frequency excursion requires a change in the "even divisor". PLL resets caused annoying "pops" in the output of direct conversion front ends that I typically use for phasing-type receivers. I was loathe to include a PLL reset on every frequency change because of that. So, I (and many others) only do a PLL reset when the required integer "even divisor" was changed. Many fewer "pops", but semi-random loss of LO quadrature within a relatively small frequency range. I added a polled front-panel switch to my hardware that I use to manually reset the PLL when my "ear detector" determines that opposite sideband suppression has gone in the toilet. There had to be a better way.
Fortunately, AD8302 phase/gain detector boards are still inexpensively available from the usual on-line sources. The test LO shown above uses one of those boards to monitor the phase offset of two 5351 outputs nominally programmed for 90 deg offset from each other. The 8302 board provided (well documented) analog outputs for phase and gain offset between its two inputs. I simply teed the 5351 output to feed both this detector and any receiver under test. A spare ADC input on the Metro Mini reads the phase offset and software (listed a the end of this post) performs a PLL reset if the ADC value is outside a prescribed tolerance band. Result: way fewer pops than doing a PLL reset for every frequency change. Disconnecting the phase offset feedback (the orange wire on the 8302 board) produced the expected continuous pops, because the software thought the phase offset was continuously out of tolerance. Since the 8302 also outputs an amplitude difference signal, there's also an opportunity to automatically control amplitude imbalance. I wasn't up for the added complexity at this point..
Some other folks seem to have implemented the same concept (QRP Labs?), but with different techniques for measuring/correcting phase offset. A casual (non-AI assisted) web search did not provide any details...
Code:
//
#include <Rotary.h>
#include <si5351.h>
#include <Wire.h>
#include "SSD1306Ascii.h"
#include "SSD1306AsciiWire.h"
// 0X3C+SA0 - 0x3C or 0x3D
#define I2C_ADDRESS 0x3c
// Define proper RST_PIN if required.
#define RST_PIN -1
SSD1306AsciiWire oled;
#define ENCODER_A 2 // Encoder pin A
#define ENCODER_B 3 // Encoder pin B
#define ENCODER_BTN 12
#define RESET_BTN 11
#define TX_ON 6
#define OFFSET_ON 8
#define PHASE_READ A2
Si5351 si5351;
Rotary r = Rotary(ENCODER_A, ENCODER_B);
volatile uint32_t cw_offset = 0ULL;
volatile uint32_t cwo_old = 0ULL;
volatile uint32_t radix = 1000; //start step size - change to suit
boolean changed_f = 0;
String tbfo = "";
static const long long pll_min = 38000000000ULL;
static const long long pll_max = 90000000000ULL;
static const long long bandInit = 1410000000ULL;
volatile long long vfo = bandInit / SI5351_FREQ_MULT;
int multiple;
float pll_freq;
bool reset_pll;
bool tx_mode;
bool offset_mode;
ISR(PCINT2_vect) {
unsigned char result = r.process();
if (result == DIR_CW)
set_frequency(1);
else if (result == DIR_CCW)
set_frequency(-1);
}
/* Change the frequency */
/* dir = 1 Increment */
/* dir = -1 Decrement */
/**************************************/
void set_frequency(short dir) {
if (dir == 1)
vfo += radix;
if (dir == -1)
vfo -= radix;
changed_f = 1;
}
/**************************************/
/* Read the button with debouncing */
/**************************************/
boolean get_button() {
if (!digitalRead(ENCODER_BTN)) {
delay(20);
if (!digitalRead(ENCODER_BTN)) {
while (!digitalRead(ENCODER_BTN));
return 1;
}
}
return 0;
}
/**************************************/
/* Displays the frequency */
/**************************************/
void display_frequency() {
uint16_t f, g;
//oled.clear(PAGE);
oled.setCursor(0, 0);
f = vfo / 1000000; //variable is now vfo instead of 'frequency'
if (f < 10)
oled.print(' ');
oled.print(f);
oled.print('.');
f = (vfo % 1000000) / 1000;
if (f < 100)
oled.print('0');
if (f < 10)
oled.print('0');
oled.print(f);
//oled.print('.');
f = vfo % 1000;
if (f < 100)
oled.print('0');
if (f < 10)
oled.print('0');
oled.print(f);
oled.setCursor(60, 0);
oled.print("MHz");
oled.setCursor(0, 3);
oled.print(tbfo);
//Serial.println(vfo + bfo);
//Serial.println(tbfo);
}
/**************************************/
/* Displays the frequency change step */
/**************************************/
void display_radix() {
oled.setCursor(84, 0);
switch (radix) {
case 1:
oled.print(" 1");
break;
case 10:
oled.print(" 10");
break;
case 100:
oled.print(" 100");
break;
case 1000:
oled.print(" 1k");
break;
case 10000:
oled.print(" 10k");
break;
case 100000:
//oled.setCursor(10, 40);
oled.print(" 100k");
break;
case 1000000:
//oled.setCursor(0, 40);
oled.print("1000k"); //1MHz increments
break;
}
oled.print("Hz");
}
void GetPLLFreq()
{
if (vfo < 6850000) {
multiple = 126;
}
if ((vfo >= 6850000) && (vfo < 9500000)) {
multiple = 88;
}
if ((vfo >= 9500000) && (vfo < 13600000)) {
multiple = 64;
}
if ((vfo >= 13600000) && (vfo < 17500000)) {
multiple = 44;
}
if ((vfo >= 17500000) && (vfo < 25000000)) {
multiple = 34;
}
if ((vfo >= 25000000) && (vfo < 36000000)) {
multiple = 24;
}
if ((vfo >= 36000000) && (vfo < 45000000)) {
multiple = 18;
}
if ((vfo >= 45000000) && (vfo < 60000000)) {
multiple = 14;
}
if ((vfo >= 60000000) && (vfo < 80000000)) {
multiple = 10;
}
if ((vfo >= 80000000) && (vfo < 100000000)) {
multiple = 8;
}
if ((vfo >= 100000000) && (vfo < 146600000)) {
multiple = 6;
}
if ((vfo >= 150000000) && (vfo < 220000000)) {
multiple = 4;
}
pll_freq = multiple * (vfo * SI5351_FREQ_MULT);
// Serial.println(vfo / 1.);
// Serial.println(pll_freq / 100000000, 9);
// Serial.println(multiple);
// Serial.println(multiple & 2);
}
void send_freq() {
int old_mult = multiple;
GetPLLFreq();
si5351.set_pll(pll_freq, SI5351_PLLA);
//si5351.set_freq_manual((vfo * SI5351_FREQ_MULT), pll_freq, SI5351_CLK0);
//si5351.set_freq_manual((vfo * SI5351_FREQ_MULT), pll_freq, SI5351_CLK2);
si5351.set_freq_manual(((vfo+cw_offset) * SI5351_FREQ_MULT), pll_freq, SI5351_CLK0);
si5351.set_freq_manual(((vfo+cw_offset) * SI5351_FREQ_MULT), pll_freq, SI5351_CLK2);
si5351.set_phase(SI5351_CLK0, 0);
si5351.set_phase(SI5351_CLK2, multiple);
if (multiple != old_mult) {
si5351.pll_reset(SI5351_PLLA);
}
tbfo = "QUAD";
}
void setup() {
Serial.begin(9600);
Wire.begin();
analogReference(INTERNAL);
pinMode(RESET_BTN, INPUT_PULLUP);
pinMode(TX_ON, INPUT_PULLUP);
pinMode(OFFSET_ON, INPUT_PULLUP);
#if RST_PIN >= 0
oled.begin(&Adafruit128x64, I2C_ADDRESS, RST_PIN);
#else // RST_PIN >= 0
oled.begin(&Adafruit128x64, I2C_ADDRESS);
#endif // RST_PIN >= 0
oled.setFont(Adafruit5x7);
oled.clear();
//initialize the Si5351
si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, 118300);
//Correction = 11000 for Adafruit board; 118500 for Chinese board
//si5351.set_pll(SI5351_PLL_FIXED, SI5351_PLLA);
si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_2MA);
si5351.drive_strength(SI5351_CLK2, SI5351_DRIVE_2MA);
// Set CLK0 to output the starting "vfo" frequency as set above by vfo = ?
send_freq();
pinMode(ENCODER_BTN, INPUT_PULLUP);
PCICR |= (1 << PCIE2); // Enable pin change interrupt for the encoder
PCMSK2 |= (1 << PCINT18) | (1 << PCINT19);
sei();
display_frequency(); // Update the display
display_radix();
}
void loop() {
//Here's where we poll the 8302 phase offset signal
int phase_v = analogRead(PHASE_READ);
if ((phase_v < 850) | (phase_v > 880)) {
si5351.pll_reset(SI5351_PLLA);
//Serial.println(phase_v);
delay(200);
}
// Update the display if the frequency has been changed
if (changed_f) {
display_frequency();
send_freq();
changed_f = 0;
}
// Button press changes the frequency change step for 1 Hz steps
if (get_button()) {
switch (radix) {
case 1:
radix = 10;
break;
case 10:
radix = 100;
break;
case 100:
radix = 1000;
break;
case 1000:
radix = 10000;
break;
case 10000:
radix = 100000;
break;
case 100000:
radix = 1000000;
break;
case 1000000:
radix = 1;
break;
}
display_radix();
}
reset_pll = digitalRead(RESET_BTN);
if (reset_pll == 0)
{
si5351.pll_reset(SI5351_PLLA);
delay(1000);
}
cwo_old = cw_offset;
offset_mode = digitalRead(OFFSET_ON);
tx_mode = digitalRead(TX_ON);
if (!offset_mode && !tx_mode)
{
cw_offset = 800ULL;
if (cw_offset != cwo_old)
{
changed_f = 1;
cwo_old = cw_offset;
}
}
else
{
cw_offset = 0ULL;
if (cw_offset != cwo_old)
{
changed_f = 1;
cwo_old = cw_offset;
}
}
}
Comments
Post a Comment