Project Implementation

Project Architecture

The goal of this project is to perform a single 20MHz running HFRCO calibration against HFXO while staying in EM1.

Once over, we let the MCU go into EM2

Implementation is mostly inspired by the one from this repo

Major changes were required to have it work on EFR32MG24 :

  • Addition of fine tuning to achieve better results
  • The HFRCOEM23 Tuning logic is the opposite of the examples' one : The higher tuning is, the lower is the output frequency

Project Code

HFRCO Initialization

By default, HFRCO will be enabled but not used as sample applications use HFXO unless otherwise configured

Therefore we need to perform simple configuration steps prior to any other, which will be put in initHFRCOEM23 :

static void initHFRCOEM23(void){
  CMU_ClockEnable(cmuClock_HFRCOEM23, true);

  HFRCOEM23->CAL = DEVINFO->HFRCOEM23CAL[HFRCOEM23_CAL_INDEX].HFRCOEM23CAL;//Calibration indexes are available in RM section 9.3.3.3
  //HFRCOEM23->CTRL |= HFRCO_CTRL_EM23ONDEMAND;
  HFRCOEM23->CTRL |= HFRCO_CTRL_FORCEEN;//We keep HFRCO enabled for debug purposes since no other peripheral consumes it
  // Drive HFRCO onto PB5 to observe calibration
  CMU_ClkOutPinConfig(2, cmuSelect_HFRCOEM23, 4, gpioPortB, 5);
}

Defines being :

#define HFRCOEM23_DESIRED_FREQ      20000000

#if (HFRCOEM23_DESIRED_FREQ == 20000000)
#define HFRCOEM23_CAL_INDEX         9
#else
#define HFRCOEM23_CAL_INDEX         8//19MHz otherwise
#endif

Defines are used to select pre-calibration values from DEVINFO page as per reference manual section 9.3.3.3 :

Silicon Labs Project Wizard Finish

As we want to observe the output frequency, we do also set it on GPIO PortB 5 (as ports A and B are the only availble ones in EM2)

CMU Initialization

CMU Calibration FSM will be based on the previously described theory of operation. Prior to starting it, it is required to set the CMU first. All state variables are initialized in dedicated resetCmuCalibrationFsm, except theoreticalUpcount which is calculated once in initCmuCalibrationFsm.

Finally, we enable CMU_IEN_CALRDY interrupt to perform this without involving the MCU and remain in EM1

The setup code is loacted in initCmuCalibrationFsm :

static void initCmuCalibrationFsm(void)
{
  resetCmuCalibrationFsm();

  // Determine ideal up count based on the desired frequency
  theoreticalUpcount = (uint32_t)(((float)HFRCOEM23_DESIRED_FREQ / (float)SystemHFXOClockGet()) * (float)(DOWNCOUNT + 1));

  // Setup the calibration circuit
  CMU_CalibrateConfig(DOWNCOUNT, cmuSelect_HFXO, cmuSelect_HFRCOEM23);

  // Enable continuous calibration
  CMU_CalibrateCont(true);

  // Enable calibration ready interrupt
  CMU_IntClear(_CMU_IF_MASK);
  CMU_IntEnable(CMU_IEN_CALRDY);
  NVIC_ClearPendingIRQ(CMU_IRQn);
  NVIC_EnableIRQ(CMU_IRQn);

  calibrationState = HFRCO_CALIBRATION_IDLE;
}

Where resetCmuCalibrationFsm is :

static void resetCmuCalibrationFsm(void)
{
  calibrationState = HFRCO_CALIBRATION_OFF;
  tuned = false;
  finetuned = false;
  lastCalcUpcountGT = false;
  lastCalcUpcountLT = false;
  currentTuningVal = 0;
  newTuningVal = 0;
  previousUpcount = 0;
  tuningOver = &tuned;
}

For this example we will set downcount to its max value. This will provide us with a worst case scenario for time and power consumption.

Depending on accuracy requirements, it is possible to reduce this amount of ticks.

As mentioned above, Silicon Labs Series 2 HFRCO has a HFRCOEM23->CAL.FINETUNE register field allowing for an even better tune of the HFRCO frequency. We defined PERFORM_FINETUNE to enable a second round of tuning using that specific register.

Defines used are set to :

#define DOWNCOUNT                   0xFFFFF
#define PERFORM_FINETUNE            1

Global variables are also required in this context :

// Global variables used in calibration ISR // TODO - could be an object like typedef struct
static HFRCO_Calibration_State_t calibrationState;
static bool tuned, finetuned, lastCalcUpcountGT, lastCalcUpcountLT;
static uint32_t theoreticalUpcount, previousUpcount, currentTuningVal, newTuningVal;

Finally, we define a pointer to a boolean which will allow us to reuse the same FSM for both tuning and fine tuning :

bool *tuningOver;

Once all the settings are covered, we need to implement the actual algorithm that will perform tuning and finetuning using the CMU ISR.

Tuning FSM

The state machine is defined in the app_process_action function:

  switch (calibrationState) {
    case HFRCO_CALIBRATION_IDLE:
      if(!tuned){
        // Get current tuningVal value
        currentTuningVal = CMU_OscillatorTuningGet(cmuOsc_HFRCOEM23);
        calibrationState = HFRCO_CALIBRATION_TUNING;
        // Start the up counter
        CMU_CalibrateStart();
      }
      break;
    case HFRCO_CALIBRATION_TUNING:
      if(tuned)
      {
        if(PERFORM_FINETUNE)
        {
          resetCmuCalibrationFsm();
          tuningOver = &finetuned;
          // Get current fine tuning value
          currentTuningVal = CMU_OscillatorFineTuningGet(cmuOsc_HFRCOEM23);
          calibrationState = HFRCO_CALIBRATION_FINE_TUNING;
          // Start the up counter
          CMU_CalibrateStart();
        } else {
          calibrationState = HFRCO_CALIBRATION_DONE;
        }
      }
      break;
    case HFRCO_CALIBRATION_FINE_TUNING:
      if(finetuned)
      {
        calibrationState = HFRCO_CALIBRATION_DONE;
      }
      break;
    case HFRCO_CALIBRATION_DONE:
      break;
    default:
      break;
  }

It is made of 3 main elements :

  • calibrationState which holds the current state of the tuning FSM
  • tuned which informs if tuning has been performed
  • finetuned which informs if fine tuning has been performed

TODO : Implementation can be much more optimized and refactored

The FSM is currently defined as below :

Silicon Labs Project Wizard Finish

EMLIB extension

Silicon Labs' EMLIB CMU module does not provide FineTuning getter and setter

We therefore created thos in app.c based on the tuning ones :

/***************************************************************************//**
 * @brief
 *   Get oscillator frequency fine tuning setting.
 *
 * @param[in] osc
 *   Oscillator to get fine tuning value for.
 *
 * @return
 *   The oscillator frequency fine tuning setting in use.
 ******************************************************************************/
uint32_t CMU_OscillatorFineTuningGet(CMU_Osc_TypeDef osc)
{
  uint32_t ret = 0U;

  switch (osc) {

    case cmuOsc_HFRCODPLL:
#if defined(CMU_CLKEN0_HFRCO0)
      CMU->CLKEN0_SET = CMU_CLKEN0_HFRCO0;
#endif
      ret = (HFRCO0->CAL & _HFRCO_CAL_FINETUNING_MASK) >> _HFRCO_CAL_FINETUNING_SHIFT;
      break;

#if defined(HFRCOEM23_PRESENT)
    case cmuOsc_HFRCOEM23:
      ret = (HFRCOEM23->CAL & _HFRCO_CAL_FINETUNING_MASK) >> _HFRCO_CAL_FINETUNING_SHIFT;
      break;
#endif

    default:
      EFM_ASSERT(false);
      break;
  }

  return ret;
}

/***************************************************************************//**
 * @brief
 *   Set the oscillator frequency finetuning control.
 *
 * @note
 *   Oscillator finetuning is done during production, and the finetuning value is
 *   automatically loaded after a reset. Changing the finetuning value from the
 *   calibrated value is for more advanced use. Certain oscillators also have
 *   build-in finetuning optimization.
 *
 * @param[in] osc
 *   Oscillator to set finetuning value for.
 *
 * @param[in] val
 *   The oscillator frequency finetuning setting to use.
 ******************************************************************************/
void CMU_OscillatorFineTuningSet(CMU_Osc_TypeDef osc, uint32_t val)
{
  switch (osc) {

    case cmuOsc_HFRCODPLL:
#if defined(CMU_CLKEN0_HFRCO0)
      CMU->CLKEN0_SET = CMU_CLKEN0_HFRCO0;
#endif
      EFM_ASSERT(val <= (_HFRCO_CAL_FINETUNING_MASK >> _HFRCO_CAL_FINETUNING_SHIFT));
      val &= _HFRCO_CAL_FINETUNING_MASK >> _HFRCO_CAL_FINETUNING_SHIFT;
      while ((HFRCO0->STATUS & HFRCO_STATUS_SYNCBUSY) != 0UL) {
      }
      HFRCO0->CAL = (HFRCO0->CAL & ~_HFRCO_CAL_FINETUNING_MASK)
                    | (val << _HFRCO_CAL_FINETUNING_SHIFT);
      break;

#if defined(HFRCOEM23_PRESENT)
    case cmuOsc_HFRCOEM23:
      EFM_ASSERT(val <= (_HFRCO_CAL_FINETUNING_MASK >> _HFRCO_CAL_FINETUNING_SHIFT));
      val &= _HFRCO_CAL_FINETUNING_MASK >> _HFRCO_CAL_FINETUNING_SHIFT;
      while ((HFRCOEM23->STATUS & HFRCO_STATUS_SYNCBUSY) != 0UL) {
      }
      HFRCOEM23->CAL = (HFRCOEM23->CAL & ~_HFRCO_CAL_FINETUNING_MASK)
                       | (val << _HFRCO_CAL_FINETUNING_SHIFT);
      break;
#endif

    default:
      EFM_ASSERT(false);
      break;
  }
}

Tuning Value Search Implementation

Tuning value search is generic to both tuning AND fine tuning as these obey the same logic

Refer to previous section as to how search is performed.

Implementation is located in computeTuning :

static uint32_t computeTuning(uint32_t calculatedUpcount_a, uint32_t previousUpcount_a, uint32_t currentTuning_a, bool *tuned_a)
{
  uint32_t tuningVal = currentTuning_a;

  /*
   * If the up counter result is less than the tuned value, the LFRCO
   * is running at a lower frequency, so the tuning value has to be
   * decremented.
   */
  if (calculatedUpcount_a < theoreticalUpcount)
  {
    // Was the up counter greater than the tuned value on the last run?
    if (lastCalcUpcountGT)
    {
      /*
       * If the difference between the ideal count and the up count
       * from this run is greater than it was on the last run, then
       * the last run produced a more accurate tuning, so revert to
       * the previous tuning value.  If not, the current value gets
       * us the most accurate tuning.
       */
      if ((theoreticalUpcount - calculatedUpcount_a) > (previousUpcount_a - theoreticalUpcount))
        tuningVal = currentTuning_a;

      // Done tuning now
      *tuned_a = true;
    }
    // Up counter for the last run not greater than the tuned value
    else
    {
      /*
       * If the difference is 1, incrementing the tuning value again
       * will only increase the frequency further away from the
       * intended target, so tuning is now complete.
       */
      if ((theoreticalUpcount - calculatedUpcount_a) == 1)
        *tuned_a = true;
      /*
       * The difference between this run and the ideal count for the
       * desired frequency is > 1, so increase the tuning value to
       * increase the LFRCO frequency.  After the next calibration run,
       * the up counter value will increase.  Save the tuning value
       * from this run; if it's close, it might be more accurate than
       * the result from the next run.
       */
      else
      {
        //prevTuning = tuningVal;
        tuningVal--;
        lastCalcUpcountLT = true;  // Remember that the up counter was less than the ideal this run
        //prevUp = calculatedUpcount_a;
      }
    }
  }

  /*
   * If the up counter result is greater than the tuned value, the
   * HFRCO is running at a higher frequency, so the tuning value has
   * to be incremented.
   */
  if (calculatedUpcount_a > theoreticalUpcount)
  {
    // Was the up counter less than the tuned value on the last run?
    if (lastCalcUpcountLT)
    {
      /*
       * If the difference between the up count and the ideal count
       * from this run is greater than it was on the last run, then
       * the last run produced a more accurate tuning, so revert to
       * the previous tuning value.  If not, the current value gets
       * the most accurate tuning.
       */
      if ((calculatedUpcount_a - theoreticalUpcount) > (theoreticalUpcount - previousUpcount_a))
        tuningVal = currentTuning_a;

      // Done tuning now
      *tuned_a = true;
    }
    // Up counter for the last run not less than the tuned value
    else
    {
      /*
       * If the difference is 1, decrementing the tuning value again
       * will only decrease the frequency further away from the
       * intended target, so tuning is now complete.
       */
      if ((calculatedUpcount_a - theoreticalUpcount) == 1)
        *tuned_a = true;
      /*
       * The difference between this run and the ideal count for the
       * desired frequency is > 1, so decrease the tuning value to
       * decrease the LFRCO frequency.  After the next calibration run,
       * the up counter value will decrease.  Save the tuning value
       * from this run; if it's close, it might be more accurate than
       * the result from the next run.
       */
      else
      {
        //prevTuning = tuningVal;
        tuningVal++;
        lastCalcUpcountGT = true;  // Remember that the up counter was greater than the ideal this run
        //prevUp = calculatedUpcount_a;
      }
    }
  }

  return tuningVal;
}

Code is available in this github repo.