etherkit / Si5351Arduino

Library for the Si5351 clock generator IC in the Arduino environment

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Speed of switching frequencies?

snout opened this issue · comments

commented

I'm trying to build a naive FSK transmitter, by sending 1ms long 'zero' (27.145.900MHz) or 'one' (27.143.000MHz) "bits", and am using the test case:
si5351.set_freq(2714590000ULL, SI5351_PLL_FIXED, SI5351_CLK0); // zero bit
delayMicroseconds(5000);
si5351.set_freq(2714300000ULL, SI5351_PLL_FIXED, SI5351_CLK0); // one bit
delayMicroseconds(1000);
But it seems like the minimum time in my pulses is 4ms output. When checking with 3MHz and 7MHz (to take two random, far-apart frequencies that my 16Msps scope can handle) I see that the minimum time for one pulse (or burst of the same frequency) is a little over 3ms, regardless of how low I drop the delayMicroseconds.

Is there a clever way to switch frequencies, considering that I only have one output that I will be using, and only two fixed frequencies (or one base frequency with +/- of some kHz) that will be sent?
Is set_freq itself a slow routine and not meant to be used for this purpose, but some other trickery to change the output faster, once the PLL frequency has been set?

There's no doubt that set_freq() was written with ease of tuning prioritized over tuning speed, and your tuning speed sounds similar to my experiences. This issue has been raised previously in a slightly different form ( #5 ), so I am aware that it's something which could be improved.

I'm not sure when I can get to the implementation of #5, but it is fairly high on my list of features to add.

In the mean time, which Arduino version are you using to drive the Si5351? If you are using a 8 or 16 MHz microcontroller, I wonder if you wouldn't see a fair bit of speed improvement in trying a Due or a Zero, which has much greater clock speed. I haven't profiled the code, but I suspect most of the time spent in the tuning routine is in doing math, so a faster micro would probably help. If you can't try it with a faster micro, let me know and I'll see if I can find some time to run an experiment here.

commented

I used an Arduino Uno, first.
I have now tried with an AtMEGA2560, using the same code (but switching SCL/SDA pins from A4A5 to digital20/21 marked SCL/SDA); the scope output still says that I get a minimum of 3,4ms switching time.
(Is it possible to set up PLL_A and PLL_B for two frequencies, and just tell the chip with a single command 'put this PLL as output to CLK0 now'? Or for that matter the MultiSynth thingie.. setting up two dividers there that just happen to divide the PLL frequency down to the right Hz, and toggling the CLK0 output between two of the MS outputs?)

Yes, it may be feasible to set up either of the methods you described, as I have experimented with both techniques in trying to get the Si5351 to generate PSK. You could manually set up the two different PLLs, and then switch between them using the set_ms_source() method.

Alternately, you could use the CLK1 output as your main output, set CLK0 and CLK1 as your mark and space frequencies, and then switch the output mux between the CLK0 output and the CLK1 output to get your FSK, using

si5351.set_clock_source(SI5351_CLK1, SI5351_CLK_SRC_MS);

and

si5351.set_clock_source(SI5351_CLK1, SI5351_CLK_SRC_MS0);

Hopefully that will give you a workaround until direct register set access can be better implemented. Good luck!

There is some intrinsic switching time when using the 2nd method where there is no output at all during the switch, so it may not be suitable for your needs. It may also affect the first method, although my memory is failing me on that at the moment. If those don't work, you may be forced to manually calculate the registers to twiddle for FSK until #5 can be implemented.

commented

Cool, thanks for the input! I'll give those a try. (As I'm just now starting to dive into the RF mire, but this sounds useful.)

commented

Comment after the fact: The Si5351 PLLs may take up to 10ms thereabouts to lock onto a new frequency when changed, so (though I haven't gotten this far yet) to get switching times well below that, the change has to happen in the MultiSynth stage for any guarantees to be made.

commented

Additional comment which solved the problem: Kicking up the Wire library speed from 100kHz to 400kHz with
Wire.setClock(400000L);
and using a bulk transfer to shove out a pre-calculated array of 8 bytes that is MultiSynth0's new divisor (instead of sending register1-byte1-register2-byte2-etc). The Si5351 supports bulk transfer with automatic address increments, so a whole new setup for MS0 was a total of 9 sent bytes. (Initial address plus the eight bytes of divisor+number+denominator.) This took the minimum pulse down to about 0.4ms, well below the 1ms I required. Hurray!

@snout Excellent! Glad you could find a way to get the desired result! I recall that bulk transfer is available, but it just slipped my mind so I'm glad you reminded me of that.

I'm going to reopen this issue just so I can look into the feasibility of adding those features to the main library code.

commented

My code (on the Mega2560):
In setup:
Wire.begin();
Wire.setClock(400000L);

  • Create a set of precalculated bytes to send out, using the formula from the Si5351 library with mult, num and denominator. Also precalculate every byte such as
    precalc2[set][2] = (precalc[set][0] & 0x00030000) >> 16;
    so that no arithmetic at all will have to be performed in the send stage.

Then in the loop, when sending:
Wire.beginTransmission(SI5351_ADDRESS);
Wire.write(baseaddr);
Wire.write(precalc2[set][0]);
Wire.write(precalc2[set][1]);
Wire.write(precalc2[set][2]);
Wire.write(precalc2[set][3]);
Wire.write(precalc2[set][4]);
Wire.write(precalc2[set][5]);
Wire.write(precalc2[set][6]);
Wire.write(precalc2[set][7]);
Wire.endTransmission();
and done.

To make this a little more generic, if timing allows, I'm fairly sure that P1, P2 and P3 (the three synth components) can be shifted-and-logic-function done in the send stage, though I wanted to do as little as possible in there. When quick shifting is needed, I'm (again) fairly sure that the calculating of P1 and P2, involving multiplication, division, and several math functions, I'd suggest an array of a few P1P2P3 tuples that can be precalculated, then sent with just a reference to which tuple is to be the new MS-configuration.

I'm working on a inexpensive VNA using the TI MSP432 and Energia, and trying to figure out how to make sweeps faster and more reliable. Here is my code:

`void setup()
{
int i;

Serial.begin(115200);
adc14_main(); // Initialize ADC14 for multi-channel conversion at 500 kHz.
si5351.init(SI5351_CRYSTAL_LOAD_8PF, 0, 0);
// For debugging 1/4/2018
si5351.drive_strength(SI5351_CLK0, SI5351_DRIVE_8MA);
si5351.set_ms_source(SI5351_CLK0, SI5351_PLLA); // Set one PLL to each clock so you can go to 200 MHz.
si5351.set_ms_source(SI5351_CLK2, SI5351_PLLB);
// Initialize the data parser using the start, end and delimiting character
Serial.begin(115200);
// For frequency sweep: "^SWEEP,Fmin,Fmax,NFreq$"
dcp.addParser("SWEEP", sweepFreqMeas);
// Returns single frequency measurements as a function of time:  "^TIME,Freq$"
dcp.addParser("TIME", voltageMeasurement);
// Returns the sample rate:  "^SAMPLERATE,1=TIME/0=COMPUTE$"
dcp.addParser("SAMPLERATE", sendSampleRate);
// Returns the dft around 155 HZ with a bin of about 10 HZ:  "^COMPUTE,Fs$"
dcp.addParser("COMPUTE", computeMeasurement);

dcp.addParser("TIMECOMPUTE", timeCompute);

setOscillator(10000000);
Serial.println("Done with setup.");
for(i=0;i<1000;i++)
{
    Serial.println(i);
}

}`

And the function that changes frequency by being called over and over again.
`void setOscillator (unsigned long long freq) // freq in Hz
{
si5351.set_freq(freq100ULL, SI5351_CLK0);
si5351.set_freq(freq
100ULL-100ULL*F_IF, SI5351_CLK2); // LO_I

delay(500); // Wait for oscillator and steady state.  Do we need this?

}`

I would love to remove the delay(500), but if even cut it down very much it ends up picking daisies after a few iterations.
Any advice?

Thanks,

Rob

@snout I'd love to see your actual Arduino code. I'm looking to retrofit an old crystal-based scanner with an Etherkit Si5351. The Arduino will set it's output based on what channel is being scanned, so I've got to tune it pretty quickly. I'd love to tune by your method, but am having trouble understanding how to go from say the settings given by Clock Builder Plus to what needs to go into the Arduino sketch...

Thanks much and 73,
Ben, KD5BYB

The code is here on github. As I recall, we tuned it down to the minimum I could get away with, and it was much less than the 500 mS above. Here is the URL:
https://github.com/frohro/WWU-VNA
73,
Rob
KL7NA

@frohro Hi Rob - excellent, thanks much! I'll take a look at this shortly. :)

thanks much and 73,
Ben, KD5BYB

commented

@frohro Hi Rob - excellent, thanks much! I'll take a look at this shortly. :)

thanks much and 73,
Ben, KD5BYB

Did the above replies help? What I ended up doing was to have the two PLLs in the chip, PLL_A and PLL_B set to different frequencies that were just 15kHz above and below the 'target' 27,145MHz "center" that the receiver expected. I used the Clock Builder to calculate the frequencies so that the divider was an integer, and had to do this because the receiver I was toying with was very picky, and the divider in the Si chip is - even mentioned in the datasheet - prone to give a little jitter when dividing by a float instead of 'doing it in integer mode'.

So the end result became: Set the divider to a common integer that fits both frequencies, then set PLL_A and PLL_B to those, and when transmitting data, simply send a command to the chip 'switch to PLL so-and-so as input now!', in effect using the two PLLs to represent mark and space and never changing the PLL frequencies or the MultiSynth divider settings. Like NT7S said further up in the thread: "You could manually set up the two different PLLs, and then switch between them using the set_ms_source() method."

Simply sending the 'change PLL' command is also a very quick operation, so that works out just fine for me. I noted above that I had to use a Mega2560 because the comparatively slow Arduino Uno at 8MHz wasn't fast enough to send the 'change PLL' commands in time for them to still be within the receiver's expected timeout-waiting-for-valid-bit.

Well! I hope this gives some insight in what I did and for what convoluted reasons. The end result - a jitter free transmission that worked for my single intended purpose, but not suitable for quickly shifting to other frequencies because of its construction. (As in, shifting requires a lot more commands than just 'switch clock source'.

As for your question about setting up the divider/multisynth, here's the few lines of code that does that for me (this all taken from either examples found in libraries, or in the library code itself, and the multiplier, number and denominator algorithms I think I got from the Si datasheet):
[Note: all this is done in setup() of my arduino code, since it's never meant to change.]
For PLL_A:
// Set up PLL_A
mult = 28; // space freq.
num = 5741; // for 27,143.5
denom = 25000;
/* Setup PLL_A /
P1 = (uint32_t)(128 * mult + floor(128 * ((float)num/(float)denom)) - 512);
P2 = (uint32_t)(128 * num - denom * floor(128 * ((float)num/(float)denom)));
P3 = denom;
/
Get the appropriate starting point for the PLL registers /
baseaddr = 26;
/
The datasheet is a nightmare of typos and inconsistencies here! /
write8( baseaddr, (P3 & 0x0000FF00) >> 8);
write8( baseaddr+1, (P3 & 0x000000FF));
write8( baseaddr+2, (P1 & 0x00030000) >> 16);
write8( baseaddr+3, (P1 & 0x0000FF00) >> 8);
write8( baseaddr+4, (P1 & 0x000000FF));
write8( baseaddr+5, ((P3 & 0x000F0000) >> 12) | ((P2 & 0x000F0000) >> 16) );
write8( baseaddr+6, (P2 & 0x0000FF00) >> 8);
write8( baseaddr+7, (P2 & 0x000000FF));
[after setting up PLL_B also]
/
Reset both PLLs, as per data sheet */
write8(SI5351_REGISTER_177_PLL_RESET, (1<<7) | (1<<5) );

For the multiSynth divider:
// init the Multisynth (fancy word for frequency divisor) on the clock chip, for a static integer division by 26
// set up MultiSynth0 at integer divisor 26
div = 26;
num = 0;
denom = 1;
P1 = (uint32_t)(128 * div + floor(128 * ((float)num/(float)denom)) - 512);
P2 = (uint32_t)(128 * num - denom * floor(128 * ((float)num/(float)denom)));
P3 = denom;
baseaddr = SI5351_REGISTER_42_MULTISYNTH0_PARAMETERS_1;
Wire.beginTransmission(SI5351_ADDRESS);
Wire.write(baseaddr);
Wire.write((P3 & 0x0000FF00) >> 8);
Wire.write(P3 & 0x000000FF);
Wire.write((P1 & 0x00030000) >> 16);
Wire.write((P1 & 0x0000FF00) >> 8);
Wire.write(P1 & 0x000000FF);
Wire.write(((P3 & 0x000F0000) >> 12) | ((P2 & 0x000F0000) >> 16));
Wire.write((P2 & 0x0000FF00) >> 8);
Wire.write(P2 & 0x000000FF);
Wire.endTransmission();
// end setup of Multisynth

..and when transmitting:
// all done creating the packet, now send it out.
write8(SI5351_REGISTER_3_OUTPUT_ENABLE_CONTROL, 0xFE); // enable output 0
delay(2); // just because .. plenty time for the output to be enabled.

[transmitting bits in a loop using this code - note that I had to tweak the timing between bits aswell to get it just right. Not sure if that value will create 1ms on any board, or if it varies with, well, temperature or what have you other conditions.]

void send_bit_with_multisynth(byte the_bit) {
if (the_bit == 0) {
write8(SI5351_REGISTER_16_CLK0_CONTROL, 0x6F); // PLL_B for input
} else {
write8(SI5351_REGISTER_16_CLK0_CONTROL, 0x4F); // PLL_A for input
}
// found with scope, for my Arduino I need to wait this long for one pulse to
be 1ms long in total.
delayMicroseconds(873);

[and finish with]
write8(SI5351_REGISTER_3_OUTPUT_ENABLE_CONTROL, 0xFF); // disable output 0

Thanks Ben,
That could be useful. I don't need really precise frequencies, so could reduce the jitter using your ideas. They also could be good for FSK operation of a TX.
Thanks for posting.
Rob

Good morning all,

Thanks much for the multiple helpful replies! I'm still working my way thru them, the datasheet, and the AN619 application note. It's going pretty slowly, but I think I'm making progress. ;)

I also came across code by Christophe, OE1CGS, where everything is done with a minimum of libraries - just the Wire.h library in fact. :) I'll post a link and the code below just for completeness:

https://www.qrp-labs.com/synth/oe1cgs.html

Please excuse the below for not being in a code box. I put in between the marks and it doesn't work? Strange, I must be doing something wrong.

Anyways, thanks much the replies!

Thanks and 73,
Ben, KD5BYB

`
#include <Wire.h>

void setup() {
Wire.begin(); // Initialize I2C-communication as master
// SDA on pin ADC04
// SCL on pin ADC05
SetFrequency (10140000); // Set TX-Frequency [10,14 MHz]
SetParkMode (); // Intialize park mode
}

void loop() {
TX_ON(); // Switches transmitter on
SetPower(4); // Nothing usefull, just changing between different power levels
delay(10000);
SetPower(3);
delay(10000);
SetPower(2);
delay(10000);
SetPower(1);
delay(10000);
TX_OFF(); // Switches transmitter off
}

void TX_ON () { // Enables output on CLK0 and disables Park Mode on CLK1
Si5351a_Write_Reg (17, 128); // Disable output CLK1
Si5351a_Write_Reg (16, 79); // Enable output CLK0, set crystal as source and Integer Mode on PLLA
}

void TX_OFF () { // Disables output on CLK0 and enters Park Mode on CLK1
Si5351a_Write_Reg (16, 128); // Disable output CLK0
Si5351a_Write_Reg (17, 111); // Enable output CLK1, set crystal as source and Integer Mode on PLLB
}

void SetFrequency (unsigned long frequency) { // Frequency in Hz; must be within [7810 Hz to 200 Mhz]
#define F_XTAL 27005701; // Frequency of Quartz-Oszillator
#define c 1048574; // "c" part of Feedback-Multiplier from XTAL to PLL
unsigned long fvco; // VCO frequency (600-900 MHz) of PLL
unsigned long outdivider; // Output divider in range [4,6,8-900], even numbers preferred
byte R = 1; // Additional Output Divider in range [1,2,4,...128]
byte a; // "a" part of Feedback-Multiplier from XTAL to PLL in range [15,90]
unsigned long b; // "b" part of Feedback-Multiplier from XTAL to PLL
float f; // floating variable, needed in calculation
unsigned long MS0_P1; // Si5351a Output Divider register MS0_P1, P2 and P3 are hardcoded below
unsigned long MSNA_P1; // Si5351a Feedback Multisynth register MSNA_P1
unsigned long MSNA_P2; // Si5351a Feedback Multisynth register MSNA_P2
unsigned long MSNA_P3; // Si5351a Feedback Multisynth register MSNA_P3

outdivider = 900000000 / frequency; // With 900 MHz beeing the maximum internal PLL-Frequency

while (outdivider > 900){ // If output divider out of range (>900) use additional Output divider
R = R * 2;
outdivider = outdivider / 2;
}
if (outdivider % 2) outdivider--; // finds the even divider which delivers the intended Frequency

fvco = outdivider * R * frequency; // Calculate the PLL-Frequency (given the even divider)

switch (R){ // Convert the Output Divider to the bit-setting required in register 44
case 1: R = 0; break; // Bits [6:4] = 000
case 2: R = 16; break; // Bits [6:4] = 001
case 4: R = 32; break; // Bits [6:4] = 010
case 8: R = 48; break; // Bits [6:4] = 011
case 16: R = 64; break; // Bits [6:4] = 100
case 32: R = 80; break; // Bits [6:4] = 101
case 64: R = 96; break; // Bits [6:4] = 110
case 128: R = 112; break; // Bits [6:4] = 111
}

a = fvco / F_XTAL; // Multiplier to get from Quartz-Oscillator Freq. to PLL-Freq.
f = fvco - a * F_XTAL; // Multiplier = a+b/c
f = f * c; // this is just "int" and "float" mathematics
f = f / F_XTAL;
b = f;

MS0_P1 = 128 * outdivider - 512; // Calculation of Output Divider registers MS0_P1 to MS0_P3
// MS0_P2 = 0 and MS0_P3 = 1; these values are hardcoded, see below

f = 128 * b / c; // Calculation of Feedback Multisynth registers MSNA_P1 to MSNA_P3
MSNA_P1 = 128 * a + f - 512;
MSNA_P2 = f;
MSNA_P2 = 128 * b - MSNA_P2 * c;
MSNA_P3 = c;

Si5351a_Write_Reg (16, 128); // Disable output during the following register settings
Si5351a_Write_Reg (26, (MSNA_P3 & 65280) >> 8); // Bits [15:8] of MSNA_P3 in register 26
Si5351a_Write_Reg (27, MSNA_P3 & 255); // Bits [7:0] of MSNA_P3 in register 27
Si5351a_Write_Reg (28, (MSNA_P1 & 196608) >> 16); // Bits [17:16] of MSNA_P1 in bits [1:0] of register 28
Si5351a_Write_Reg (29, (MSNA_P1 & 65280) >> 8); // Bits [15:8] of MSNA_P1 in register 29
Si5351a_Write_Reg (30, MSNA_P1 & 255); // Bits [7:0] of MSNA_P1 in register 30
Si5351a_Write_Reg (31, ((MSNA_P3 & 983040) >> 12) | ((MSNA_P2 & 983040) >> 16)); // Parts of MSNA_P3 und MSNA_P1
Si5351a_Write_Reg (32, (MSNA_P2 & 65280) >> 8); // Bits [15:8] of MSNA_P2 in register 32
Si5351a_Write_Reg (33, MSNA_P2 & 255); // Bits [7:0] of MSNA_P2 in register 33
Si5351a_Write_Reg (42, 0); // Bits [15:8] of MS0_P3 (always 0) in register 42
Si5351a_Write_Reg (43, 1); // Bits [7:0] of MS0_P3 (always 1) in register 43
Si5351a_Write_Reg (44, ((MS0_P1 & 196608) >> 16) | R); // Bits [17:16] of MS0_P1 in bits [1:0] and R in [7:4]
Si5351a_Write_Reg (45, (MS0_P1 & 65280) >> 8); // Bits [15:8] of MS0_P1 in register 45
Si5351a_Write_Reg (46, MS0_P1 & 255); // Bits [7:0] of MS0_P1 in register 46
Si5351a_Write_Reg (47, 0); // Bits [19:16] of MS0_P2 and MS0_P3 are always 0
Si5351a_Write_Reg (48, 0); // Bits [15:8] of MS0_P2 are always 0
Si5351a_Write_Reg (49, 0); // Bits [7:0] of MS0_P2 are always 0
if (outdivider == 4){
Si5351a_Write_Reg (44, 12 | R); // Special settings for R = 4 (see datasheet)
Si5351a_Write_Reg (45, 0); // Bits [15:8] of MS0_P1 must be 0
Si5351a_Write_Reg (46, 0); // Bits [7:0] of MS0_P1 must be 0
}
Si5351a_Write_Reg (177, 32); // This resets PLL A
}

void SetParkMode () { // Sets CLK1 to the Park Mode frequency of 150 MHz to keep the Si5351a warm during key-up
Si5351a_Write_Reg (17, 128); // Disable output during the following register settings
Si5351a_Write_Reg (34, 255); // Bits [15:8] of MSNB_P3
Si5351a_Write_Reg (35, 254); // Bits [7:0] of MSNB_P3
Si5351a_Write_Reg (36, 0); // Bits [17:16] of MSNB_P1 in bits [1:0]
Si5351a_Write_Reg (37, 14); // Bits [15:8] of MSNB_P1
Si5351a_Write_Reg (38, 169); // Bits [7:0] of MSNB_P1
Si5351a_Write_Reg (39, 252); // Parts of MSNB_P3 und MSNB_P1
Si5351a_Write_Reg (40, 130); // Bits [15:8] of MSNB_P2
Si5351a_Write_Reg (41, 82); // Bits [7:0] of MSNB_P2
Si5351a_Write_Reg (50, 0); // Bits [15:8] of MS1_P3
Si5351a_Write_Reg (51, 1); // Bits [7:0] of MS1_P3
Si5351a_Write_Reg (52, 0); // Bits [17:16] of MS1_P1 in bits [1:0] and R in [7:4]
Si5351a_Write_Reg (53, 1); // Bits [15:8] of MS1_P1
Si5351a_Write_Reg (54, 0); // Bits [7:0] of MS1_P1
Si5351a_Write_Reg (55, 0); // Bits [19:16] of MS1_P2 and MS1_P3
Si5351a_Write_Reg (56, 0); // Bits [15:8] of MS1_P2
Si5351a_Write_Reg (57, 0); // Bits [7:0] of MS1_P2
Si5351a_Write_Reg (177, 128); // This resets PLL B
}

void SetPower (byte power){ // Sets the output power level
if (power == 0 || power > 4){power = 0;} // valid power values are 0 (25%), 1 (50%), 2 (75%) or 3 (100%)
switch (power){
case 1:
Si5351a_Write_Reg (16, 76); // CLK0 drive strength = 2mA; power level ~ -8dB
break;
case 2:
Si5351a_Write_Reg (16, 77); // CLK0 drive strength = 4mA; power level ~ -3dB
break;
case 3:
Si5351a_Write_Reg (16, 78); // CLK0 drive strength = 6mA; power level ~ -1dB
break;
case 4:
Si5351a_Write_Reg (16, 79); // CLK0 drive strength = 8mA; power level := 0dB
break;
}
}

void Si5351a_Write_Reg (byte regist, byte value){ // Writes "byte" into "regist" of Si5351a via I2C
Wire.beginTransmission(96); // Starts transmission as master to slave 96, which is the
// I2C address of the Si5351a (see Si5351a datasheet)
Wire.write(regist); // Writes a byte containing the number of the register
Wire.write(value); // Writes a byte containing the value to be written in the register
Wire.endTransmission(); // Sends the data and ends the transmission
}
`