Using runtime started LFCLK external oscillator with nRF5340 and Bluetooth enabled

Using SDK 2.5.1 at time of writing. 

This is less a "request for help" and more of a "here's how I managed to do this and I'm documenting it here in case others want to do the same thing". 

The existing available configuration settings and APIs aren't really set up to allow you to easily use an external oscillator as an LFCLK source when using an nRF5340 processor that gets kicked off at runtime, mostly because the zephyr kernel needs some kind of LFCLK source for use in its kernel at startup and that it also assumes it will not need to be switched at runtime. If you need an external oscillator for your design (such as using an external calendar chip with a backup battery whose time you want to stay synchronized to the processor while running), here's how to do it anyway. Note that you'll have to make adjustments as needed if your project setup is different from mine. You'll of course need to make sure that the low frequency external oscillator is connected to the XL1 pin as usual, and that XL2 is left floating. 

You'll have to tell the SDK that you want the Bluetooth image loaded into the network core with the Bluetooth hci interface enabled. Easiest way to do that is make sure you have CONFIG_BT=y set in your project configuration file, along with all the other configuration settings you'd normally set up. I recommend starting with a sample application and modifying it if you're doing this for the first time. 

In your build configuration settings, you'll need to lie to the network core and tell it that the "XTAL" interface will be available from startup (we'll fix the obvious consequence of this lie later). In your build configuration settings add -Dhci_rpmsg_CONFIG_CLOCK_CONTROL_NRF_K32SRC_XTAL="y" and -Dhci_rpmsg_CONFIG_CLOCK_CONTROL_NRF_K32SRC_20PPM="y" to the Extra CMake arguments section (or the appropriate PPM level as needed). If alternatively if you've gone the path of linking the network core blob as a child image of your project, you can put these settings in the child image configuration file instead. 

Now we'll need to set up the clock configuration for the main application image. In your project configuration settings, set CONFIG_SOC_ENABLE_LFXO=y, CONFIG_SOC_LFXO_CAP_EXTERNAL=n, CONFIG_CLOCK_CONTROL_NRF_K32SRC_SYNTH=y, and CONFIG_CLOCK_CONTROL_NRF_K32SRC_20PPM=y. Essentially we're telling the system to set up the GPIO for external clock use with no capacitors, but at startup use the synthesized clock from the high frequency clock source for our low frequency clock source. We can't tell it to use the RC clock, because then the kernel drivers would schedule a periodic calibration for a clock source that isn't running, which is the same reason we told the network core to use something other than RC clock. Your chip will randomly crash later if you try to use RC clock instead. We also couldn't tell main application to use the external clock source on startup because it's not running yet and the kernel will hang before reaching the main function. Therefore synthesized clock is the only workable option for startup configuration, but we don't care about its inaccuracy or non-compatibility with Bluetooth because we'll be switching away from it almost immediately. 

Edited: If you have a bootloader, you must set it to use the SYNTH clock at startup like the application. If you require network core calls from bootloader (such as over the air update), you'll also need to perform a similar runtime correction to what we're about to do for our application code below. 

We're ready to modify our init routines to handle our unusual clock setup. The first thing we'll want to do at startup is to disable the network core using the network force off bit in the reset register (see datasheet). It is unable to start up right now anyway due to expecting an external clock that isn't running yet. Next we'll want to get our external oscillator started, this is just whatever the normal process is for your hardware design whether it's enabled via a GPIO pin or via a set of serial transactions. Once you get it running, you'll then want to tell the chip that you're using an external oscillator instead of an external crystal via the oscillator bypass register in the oscillators peripheral (again see datasheet), just write the bit to set it to rail to rail external source. When that's done, you'll finally perform the switchover to use the external oscillator. Set up the transition by setting the LFCLKSRC register in the clock controller registers to "xtal", which will really be external oscillator due to the bypass register adjustment we just made. Use a barrier to make sure the processor does not perform the next write out of order before the previous register settings. Finally, set the start trigger bit in the LFCLK start register to tell the peripheral to perform the transition over to the new clock source. Per the datasheet you may lose up to 1 LFCLK clock cycle during this transition, but that shouldn't be a problem for an initialization routine like this. 

Due to the way the RTC clock source configuration is set up inside the nRF53, the last operation has switched the input of ALL RTC peripherals over to the external source, including the ones used by the kernel and the network core. So the last thing we have to do is remember that we had shut off the network core earlier and now we need to restart it. Clear the network force off bit now to get it reset and running again. Now you have a fully up and running system that will use the external oscillator as its low frequency clock source and be compatible with the Bluetooth core running, so you can perform the rest of your normal init routine and run as usual from here. 

Edited: Final important note is that if you choose to reset the chip using NVIC_SystemReset, you'll want to shut off the network core, then switch back to SYNTH clock and wait for ready before calling the reset function. 

  • Hello,

    Thank you for the detailed write-up. I'm wondering if it would be possible to initiate the external clock source before the pre-kernel initialization here: https://github.com/nrfconnect/sdk-zephyr/blob/272786b074227b4abcbe8f954d5bb8acf54f3dd3/drivers/timer/nrf_rtc_timer.c#L771 which is requesting the LF clock.

    You'll of course need to make sure that the low frequency external oscillator is connected to the XL1 pin as usual, and that XL2 is left floating. 

    For the nRF52 series we had the following Kconfig symbols to enable support for external clock source in the clock driver: https://github.com/nrfconnect/sdk-zephyr/blob/272786b074227b4abcbe8f954d5bb8acf54f3dd3/drivers/clock_control/Kconfig.nrf#L45. Support for the nRF53 does not appear to have been added yet. 

    Did you set the bypass register directly?  

    Low-frequency (32.768 kHz) crystal oscillator (LFXO) 

    Best regards,

    Vidar

  • It is probably possible to start the external clock pre-kernel in many cases. There's just no general way to do it that could be added to the SDK files because what's actually required to start up your external clock is heavily dependent on your exact hardware setup. Mine for example needs both a GPIO write to bring up power to the oscillator, plus some extra I2C serial transactions to set it to the correct speed. Others might just be started up automatically with board power and never need a manual kickoff. The limitation when doing it pre-kernel is that you can't use any resources that rely on LFCLK to be running as part of your external oscillator startup procedure, and if you do need those resources then your only option is to start up on SYNTH and do a manual switchover later. If there was a way to suppress periodic calibration of the RC clock then that would have been an option as well, given that you don't actually need regular calibration in this situation, but that would definitely require some changes to the SDK to become available as an option. 

    For bypass, yes I had to resort to setting the bypass register directly while taking into account the base pointer difference between secure and non-secure application modes. I do this just before modifying the LFCLK source and kicking the start trigger. My code for that part looks basically like this:

    // Set address of registers based on secure vs non-secure application mode
    #if APP_NS
    static const uint32_t oscillators_base_addr = 0x40004000;
    static const uint32_t oscillators_bypass_offset_addr = 0x6C0;
    static const uint32_t oscillators_bypass_enable = 0x1;
    #else
    static const uint32_t oscillators_base_addr = 0x50004000;
    static const uint32_t oscillators_bypass_offset_addr = 0x6C0;
    static const uint32_t oscillators_bypass_enable = 0x1;
    #endif
    
    // Set bypass to use "oscillator mode" instead of "crystal mode" on external pin
    uint32_t volatile *p_osc_bypass_reg = (uint32_t volatile *)(oscillators_base_addr + oscillators_bypass_offset_addr);
    *p_osc_bypass_reg = oscillators_bypass_enable;
    __DMB();
    __DSB();

  • I see. Generally, hardware specific initializations can be placed in your board files. However, getting a clean solution in this case might be difficult since configuration involves using the I2C bus. Technically, you could use the nRFx drivers directly for this, as they do not depend on the sysclock or other Zephyr resources. But I guess it might conflict with the actual initialization of the RTC later in the startup process.

    Nathan said:
    For bypass, yes I had to resort to setting the bypass register directly while taking into account the base pointer difference between secure and non-secure application modes. I do this just before modifying the LFCLK source and kicking the start trigger. My code for that part looks basically like this:

    Thanks. I just wanted to make sure. You can also include soc.h an use our register defintitions. 

    E.g.,

    NRF_OSCILLATORS_S->XOSC32KI.BYPASS = 
        (OSCILLATORS_XOSC32KI_BYPASS_BYPASS_Enabled << OSCILLATORS_XOSC32KI_BYPASS_BYPASS_Pos);

    Nathan said:
    If there was a way to suppress periodic calibration of the RC clock then that would have been an option as well, given that you don't actually need regular calibration in this situation, but that would definitely require some changes to the SDK to become available as an option

    Did you try to build with CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC_CALIBRATION=n in your appcore FW?

  • Using your register definitions in soc.h would probably be cleaner and I might switch to that later, it winds up being the same idea in the end though. 

    Earlier I tried setting that CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC_CALIBRATION=n value, but it gave me a build error in a zephyr driver. I've re-tested it now and it worked fine when setting only application and bootloader, until I stopped lying to the network core and told it to use RC clock with no calibration. Then the build error shows up in /subsys/mpsl/init/mpsl_init.c in the mpsl_calibration_work_handler function. That function only cares about the CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC definition and always tries to grab CONFIG_CLOCK_CONTROL_NRF_CALIBRATION_PERIOD, disrespecting the CALIBRATION=n setting. So network core needs to be lied to and reset after runtime clock config right now, but maybe starting on RC would work fine if only the application and bootloader were told to use it with the calibration disabled. I'll try testing that later when I get some time. In the meantime I don't think there would be any issues of just falling back to startup with SYNTH instead as long as you can stomach the extra power draw of leaving HFCLK on (in my case the bootloader is USB based so I don't care about power draw there, and init is short, so it's no issue).

    In any case starting the network core on RC to prevent having to reset it is untenable due to the build error. And additionally that might be behavior that the SDK shouldn't change, because in literally any other situation the SDK should produce a build error if the user is trying to use Bluetooth with an uncalibrated RC clock. It just happens that in this unique case I need to start up on an alternate clock source and have no intentions of leaving the network core in a state where it's running on an uncalibrated RC by the time I go to start initializing the Bluetooth services over hci_rpmsg. 

  • You are right, there is a static build assert that will be triggered if you build the network firmware without clock calibration enabled. From a HW perspective, it does not matter whether the calibration is performed on the application core or the network core. However, in the SDK, we have decided that clock calibration should be performed by the network core when BT is enabled. I thought the calibration was done by the application core (this was true in earlier SDK versions), and therefore, it would be possible to trick the BT controller into using the RC source uncalibrated.

Related