Center frequency tuning time after some change

I want to scan some wider frequency range - say 200 MHz using 50MHz window. For this, i need to capture a trace, then shift Fc for 50 MHz, capture the second trace, etc. The questions are:

  1. Do I need to perform LMS_Calibrate() routine after such Fc shift? If yes, is there (or can be created) some enlightened version of calibration, as currently it reports to take ~250 ms?
  2. Is there any sensor in LimeSDR to indicate that Fc tuning has completed? If not, how to estimate a time that it takes to tune?
  3. Do I need to stop streaming to change Fc?

Please describe the correct procedure that should be performed.

Thank you!


  1. Yes, calibration should be done for each different Fc. But if you’re going to be switching over the same Fc’s, it’s possible to calibrate at each Fc on startup and then reuse calibration results without running the calibration procedure, as long as other chip parameters are the same.
  2. Yes, there are registers indicating Fc tuning completion. When using LMS_SetLOFrequency function Fc will be already tuned upon function return. Same as with calibration, Fc tuning results can be reused for faster switching.
  3. Streaming can be running during Fc tune, but will produce random samples.

Can you refer me to some docs or examples with description how to save/restore calibration results and tuning results? Or may be you can sketch some rough pseudocode with LMS function names from LimeSuite?

I found LMS_EnableCache() func in LimeSuite.h and tried it. As I can guess it creates sqlite database and stores calibration values there. Couple questions:

  1. Are tuning values for LMS_SetLOFrequency also stored in that database automatically (so I don’t need to do anything more to have them cached also)?
  2. Are stored values supposed to have some approximate validity period? I mean is there any need to clear cache once month/year or some other time period to force recalibration, or those values will be valid for months/years/etc?

I’ll look up which registers you would need to store.
The cache was primarily ment for dc/iq calibrations storage, don’t remember if it was extended to include LO tunes.
Can’t say anything for sure about the validity period of the cache. Obviously it’s best to calibrate at startup to get best result as some factors that are not stored could have effects on the calibration, like temperature or some unusual chip setup (I have limited knowledge of the hardware internal dependencies so could be wrong)

I checked the code, so the cache has been extended to also store LO tuning values, but it was never updated to cache new analog DC calibration values, so it does not work properly.

Here’s code you can use to readback calibration results, and later restore them. Assuming the calibration has been executed at least once, so all the necessary parameter enables would be properly set by the procedure, and you would be just modifying the control values.

//PLL tune same for Rx/Tx just switch channel A(Rx) / B(Tx)
uint16_t reg011D; //FRAC_SDM[15:0]
uint16_t reg011E; //INT_SDM & FRAC_SDM[19:16]
uint16_t div_loch;
uint16_t en_div2;
uint16_t sel_vco;
uint16_t csw_vco;

//readback results
LMS_ReadLMSReg(device, 0x011D, &reg011D);
LMS_ReadLMSReg(device, 0x011E, &reg011E);
LMS_ReadParam(device, LMS7_DIV_LOCH, &div_loch);
LMS_ReadParam(device, LMS7_EN_DIV2_DIVPROG, &en_div2);
LMS_ReadParam(device, LMS7_SEL_VCO, &sel_vco);
LMS_ReadParam(device, LMS7_CSW_VCO, &csw_vco);
//restore results
LMS_WriteLMSReg(device, 0x011D, reg011D);
LMS_WriteLMSReg(device, 0x011E, reg011E);
LMS_WriteParam(device, LMS7_DIV_LOCH, div_loch);
LMS_WriteParam(device, LMS7_EN_DIV2_DIVPROG, en_div2);
LMS_WriteParam(device, LMS7_SEL_VCO, sel_vco);
LMS_WriteParam(device, LMS7_CSW_VCO, csw_vco);

// DC/IQ same for Rx/Tx just adjust the paramter names 
uint16_t gcorri;
uint16_t gcorrq;
uint16_t phaseOffset;
int16_t dci;
int16_t dcq; 

//readback results
LMS_ReadParam(device, LMS7_GCORRI_RXTSP, &gcorri);
LMS_ReadParam(device, LMS7_GCORRQ_RXTSP, &gcorrq);
LMS_ReadParam(device, LMS7_IQCORR_RXTSP, &phaseOffset);
dci = ReadAnalogDC(LMS7_DC_RXAI.address);
dcq = ReadAnalogDC(LMS7_DC_RXAQ.address);

//restore results
LMS_WriteParam(device, LMS7_GCORRI_RXTSP, gcorri);
LMS_WriteParam(device, LMS7_GCORRQ_RXTSP, gcorrq);
LMS_WriteParam(device, LMS7_IQCORR_RXTSP, phaseOffset);
WriteAnalogDC(LMS7_DC_RXAI.address, dci);
WriteAnalogDC(LMS7_DC_RXAQ.address, dcq);

int16_t ReadAnalogDC(const uint16_t addr)
	const uint16_t mask = addr < 0x05C7 ? 0x03FF : 0x003F;
	uint16_t value;
	int16_t result;
	LMS_WriteLMSReg(device, addr, 0);
	LMS_WriteLMSReg(device, addr, 0x4000);
	LMS_ReadLMSReg(device, addr, &value);
	LMS_WriteLMSReg(device, addr, value & ~0xC000);
	result = (value & mask);
	if(value & (mask+1))
	    result *= -1;
	return result;

void WriteAnalogDC(const uint16_t addr, int16_t value)
	const uint16_t mask = addr < 0x05C7 ? 0x03FF : 0x003F;
	int16_t regValue = 0;
	if(value < 0)
	    regValue |= (mask+1);
	    regValue |= (abs(value+mask) & mask);
	    regValue |= (abs(value+mask+1) & mask);
	LMS_WriteLMSReg(device, addr, regValue);
	LMS_WriteLMSReg(device, addr, regValue | 0x8000);


Thank you for the code! Though as I tried the ease of using cache :slight_smile: , it’s the whole problem to force myself to set up similar parallel caching system for those extra values… Do you know if it’s planned to add them to existing cache system?

I understand that one would need to add fields to db tables and add insert/select calls in corresponding places in code. How would you estimate this task - are those places of a limited count, or they are scattered by code and it’s non-trivial task for external person?

For anyone who may be reading this in the future: just found out this topic: Calibration procedure problem and understood that fixing calibration cache is not in Lime’s priority list, so if one needs it, he needs to do it himself. From this perspective, additional thanks to Ricardas for the parameter list for retuning.

Sorry for bumping the topic, but we just got to the point where we have to implement this technique. But we cannot succeed with saving/restoring according to the list in your post from Oct 20.
Here are comparison of two procedures that we implemented to test this (we are using LimeSDR under linux on Odroid XU4):

  1. We setup all LimeSDR params for 1.1 GHz @40MHz sample rate, calibrate the channel with LMS_Calibrate(), turn on sine wave signal over the air at 1.11 GHz and successively read traces by 40000 samples (=1 ms), looking at the DFT. In this case, we have a picture as expected (we cut out DFT areas at the center and by edges, leaving just nearly linear segments):

Y axis is in dBFS. We see an expected peak at 1.11 GHz and a lower mirror peak on the left.

  1. Now, we turn off the test signal, pre-calibrate device for a row of frequencies from 300 MHz to 3 GHz with 40 MHz step, saving the values as per your proposed list. Thus, last call to LMS_Calibrate() is at 3G. Then, we turn on the same test signal at 1.11G, tune the LO to 1.1G, restore calibration values for it, and successively repeat reading traces as above. Now we see some “bad picture”:

Now we see many peaks, the highest of them being not at our test signal freq (shifted 1 MHz right). It seems that there is something wrong with some freq dividers or smth like this. Can we somehow check that we save/restore values correctly? May be there are some additional provisions that we should make before restoring?

Also, can you please doublecheck that the value list is complete, taking into account that some time has passed since that post? Of course, we use the latest version of LimeSuite source from github.


Hi, the registers list is the same.
I suspect that highest peak is your test signal shifted by chip’s NCO, no idea why would it become active, unless you are doing something with it yourself.
It would be best if you could show me your pre-calibration code.
Does your precalibration and restoring are done during the same program run, or are you resetting/reinitializing the board before using stored values?

Ricardas, thanks for your answer, sorry for the delay! It turned out to be a false alarm, sorry about this. We doublechecked our code and there was exactly 1 MHz displacement between the center freq where calibration was done and the center freq during trace input. So, this was just our fault.

I don’t understand what you mean by just switch channel A(Rx) / B(Tx) for the PLL tune.

For DC/IQ I imagine you simply replace RX by TX in the 5 names, like LMS7_GCORRI_TXTSP.

DC/IQ and PLL tuning are separate things.
Here’s simplification of the registers map layout, each channel has it’s own Rx dc iq, Tx dc iq values, but because there is only one Rx PLL and one Tx PLL, they are layed out differently. When you are manually reading/writing register values, you use LMS7_MAC to select which register map’s channel you want to modify.
Ch.A | RxDC_A, RxIQ_A, TxDC_A, TxIQ_A, RxPLL config
Ch.B | RxDC_B, RxIQ_B, TxDC_B, TxIQ_B, TxPLL config

It’s just how the chip’s register map is set up.
PLL tuning values of receiver and transmitter are located at identical register addreses, but which one of them is being configured is controlled by the channel selection.
So if you want to manually read or write PLL tuning values of receiver, you have to first set the active channel LMS7_MAC to 1 (means channel A) and then write or read the LMS7_CSW_VCO, LMS7_SEL_VCO… values.
For transmitter it’s the same register names, only the LMS7_MAC has to be set to 2 (channel B).

Thanks Ricardas,

So when accessing PLL tune registers, you first have to set the channel (1 for RX, 2 for TX), as they share the same addresses.

I’m still confused as to whether you have to also do it for DC/IQ, considering DC has different addresses for channels A and B. Maybe it’s necessary for LMS7_GCORRI_RXTSP, LMS7_GCORRQ_RXTSP and LMS7_IQCORR_RXTSP?

Please see the code below where I read the PLL tune for RX then TX, as well as DC/IQ for RX on channel A then TX on channel B.

// For PLL tune, "channel" corresponds to either "RX" or "TX".
// It's different from the channels in mono or dual channel.

// Read PLL tune for RX.
// First, set the active channel to 1, ie A, ie RX.
LMS_WriteParam(device, LMS7_MAC, 1);
// Then read the necessary registers.
LMS_ReadLMSReg(device, 0x011D, &reg011D);
LMS_ReadLMSReg(device, 0x011E, &reg011E);
LMS_ReadParam(device, LMS7_DIV_LOCH, &div_loch);
LMS_ReadParam(device, LMS7_EN_DIV2_DIVPROG, &en_div2);
LMS_ReadParam(device, LMS7_SEL_VCO, &sel_vco);
LMS_ReadParam(device, LMS7_CSW_VCO, &csw_vco);

// Read PLL tune for TX
// First, set the active channel to 2, ie B, ie TX.
LMS_WriteParam(device, LMS7_MAC, 2);
// Then read the same registers as above.

// For DC/IQ, when specifying a parameter name, you use either RX or TX for the direction,
// as well as either A or B for the channel.

// Read DC/IQ for RX on channel A.
LMS_WriteParam(device, LMS7_MAC, 1); // We must first set channel to 1 ie A (unless it's already on that channel)
LMS_ReadParam(device, LMS7_GCORRI_RXTSP, &gcorri);
LMS_ReadParam(device, LMS7_GCORRQ_RXTSP, &gcorrq);
LMS_ReadParam(device, LMS7_IQCORR_RXTSP, &phaseOffset);
dci = ReadAnalogDC(LMS7_DC_RXAI.address);
dcq = ReadAnalogDC(LMS7_DC_RXAQ.address);

// Read DC/IQ for TX on channel B.
LMS_WriteParam(device, LMS7_MAC, 2); // We must first set channel to 2 ie B (unless it's already on that channel)
LMS_ReadParam(device, LMS7_GCORRI_TXTSP, &gcorri);      // Same as above but with TX
LMS_ReadParam(device, LMS7_GCORRQ_TXTSP, &gcorrq);      // Same as above but with TX
LMS_ReadParam(device, LMS7_IQCORR_TXTSP, &phaseOffset); // Same as above but with TX
dci = ReadAnalogDC(LMS7_DC_TXBI.address); // Same as above but with TXB
dcq = ReadAnalogDC(LMS7_DC_TXBQ.address); // Same as above but with TXB


For the PLLs this looks correct, as for DC/IQ, it depends on which channels you intend to use.
If you are using Rx channel A and Tx channel B, then yes, you will have to set LMS7_MAC before reading/writing to each channel. But If you are using the same channel Rx and Tx, then you don’t need to switch LMS7_MAC each time.

Perfect, thanks.

This works well and takes ~12ms to restore PLL and calibration for one channel, with an additional ~6ms to restore the calibration for the 2nd channel.

Considering LMS_WriteParam and LMS_WriteLMSReg both end up calling SPI_write (as we’re not using addresses 0x0640 or 0x0641), it might be possible to call SPI_write_batch. @ricardas Do you know if it’s safe to try to batch the writes, or do some or all registers need to be written in a specific order?

Eg for PLL tune we do 2 LMS_WriteLMSReg followed by 4 LMS_WriteParam, with each LMS_WriteParam first calling SPI_read. Could I do 1 SPI_read_batch to get the data for those 4 calls, and then do 1 SPI_write_batch corresponding to the 6 calls?

As for WriteAnalogDC, I guess we must keep those 2 LMS_WriteLMSReg calls following each other, but maybe we can try to also combine the first 3 LMS_WriteParam into 1 SPI_read_batch followed by 1 SPI_write_batch.

Yes, using batches to write and read is safe.

Yes, you can. Also depending on what you are doing, if the other parameter values in those registers that are being read are not changing, then there would be no need to read them every time, you could just read once in the beginning and after that only write them.

Yes, for WriteAnalogDC there needs to be 2 writes, but those can be batched too.

Not sure what you’re talking about here, WriteAnalogDC does not do this.

Thanks a lot @ricardas and sorry for the confusion on the last part. I was talking about the calibration part where we do 3 calls to LMS_WriteParam followed by 2 calls to WriteAnalogDC.

I should be able to make changing the frequency even faster.