Problem with millis() on Nano 33 BLE

I have an app that uses the Nano 33 BLE and connects via BLE to a mobile phone. One thing it needs internally is a 10mS tick, so I am creating this by using millis().

The way it works is that it notes the time from millis() in setup() - currentTime = millis(), then in loop() it polls millis() looking for a time 10mS or more further on from currentTime. On finding that it does its 10mS tasks, and increments the currentTime by 10mS, and so on.

What I see is that a cumulative error builds up - some times the gap from the previous 10mS is more than 10mS, maybe 12-13mS, but it never recovers it, so the error builds up.

So, I built a very simple test app for the Nano 33 BLE:

#include <Arduino.h>

///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////

#define TICKPERIOD				(10)	//	10mS
#define OUTPUTPIN				(12)	//	DigIO 12

///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////

unsigned long
		currentTime;
boolean	toggle;

///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////

static void driveOutput(void)

{
		digitalWrite(OUTPUTPIN, toggle ? HIGH : LOW);
		toggle = !toggle;
}

///////////////////////////////////////////////////////////////////////////

void setup()
{
	currentTime = millis();
	pinMode(OUTPUTPIN, OUTPUT);
	toggle = false;
	driveOutput();
}

///////////////////////////////////////////////////////////////////////////

void loop()
{
	unsigned long
			newTime = millis();

	if (newTime >= (currentTime + TICKPERIOD))
	{
		driveOutput();
		currentTime += TICKPERIOD;
	}

}

It worked fine, rock solid square wave on D12 inverting every 10mS. So I changed the library to ArduinoBLE.h to pull in all the BLE code, and exactly the same, works fine.

But, as soon as real BLE services and characteristics are set up, the accumulative error returns. I can only assume that something in the library is blocking out the timer interrupt for too long, and interrupts get missed.

So:

  • am I right about that?
  • is it a known issue?
  • if so, is there any way round it? I was rather relying on the 10mS tick without extra hardware.

Why not use a hardware timer for the tick? A hardware timer will not be dependent upon software.

Unfortunately the board is already there and it doesn't have any external hardware.

An Uno does have several internal timers that are available for your use.

MsTimer2 - Arduino Reference, is one library I've used in the past, works good.

For ESP32's I just directly use the 4 available hardware timers.

Thanks, I'll have a look, though I notice in the library header file it says for AVR architectures only. Think the Nano 33 BLE has some nRF chipset, so not sure if it'll be applicable.

Words like "nano ble33 hardware timer library" did not give you more info?

Looking at NRF52_MBED_TimerInterrupt now.

The most simple way to use the hardware timers for periodic functions is to use the Ticker Class

From any sketch you can call mbed APIs by prepending mbed:: namespace.

The compiler gives warnings which can be eliminated by also using the chrono namespace with Ticker.

Here's your code recrafted to use Ticker

#include <Arduino.h>
#include "mbed.h"
using namespace std::chrono;//compiler shows no warnings
mbed::Ticker myTimer;

#define TICKPERIOD      10ms  //  using chrono namespace syntax
#define OUTPUTPIN        12  //  DigIO 12 
boolean  toggle;

static void driveOutput(void)

{
    digitalWrite(OUTPUTPIN, toggle ? HIGH : LOW);
    toggle = !toggle;
}

///////////////////////////////////////////////////////////////////////////

void setup()
{
 
 pinMode(OUTPUTPIN, OUTPUT);
  toggle = false;
 // driveOutput();
 myTimer.attach(&driveOutput,TICKPERIOD);
}

void loop()
{
  
}

Thanks. I tried that, seems to suffer from the same problem of masked interrupts and accumulated errors. Again, it is fine providing the BLE services and characteristics are not set up.

Thanks @cattledog for the PM about this issue.

@nmw01223

I suggest you have a look at ISR_16_Timers_Array_Complex on Arduino Nano_33_BLE and see why software-based timer (such as Ticker, SimpleTimer, BlynkTimer, etc.) can't guarantee the time correctness when the system is busy doing something else. Using Timer Interrupt can help solve the issue, but certainly with some catch requiring you to use ISR correctly.

'currentTime' is not 'currentTime' if updated only one time in the setup loop.

@nmw01223

This is the sample sketch for you to show how to start

#if !( ARDUINO_ARCH_NRF52840 && TARGET_NAME == ARDUINO_NANO33BLE )
  #error This code is designed to run on nRF52-based Nano-33-BLE boards using mbed-RTOS platform! Please check your Tools->Board setting.
#endif

// These define's must be placed at the beginning before #include "NRF52TimerInterrupt.h"
// _TIMERINTERRUPT_LOGLEVEL_ from 0 to 4
// Don't define _TIMERINTERRUPT_LOGLEVEL_ > 0. Only for special ISR debugging only. Can hang the system.
// For Nano33-BLE, don't use Serial.print() in ISR as system will definitely hang.
#define TIMER_INTERRUPT_DEBUG         0
#define _TIMERINTERRUPT_LOGLEVEL_     3

// To be included only in main(), .ino with setup() to avoid `Multiple Definitions` Linker Error
#include "NRF52_MBED_TimerInterrupt.h"

// For core mbed core 1.3.2-
// Depending on the board, you can select NRF52 Hardware Timer from NRF_TIMER_1,NRF_TIMER_3,NRF_TIMER_4 (1,3 and 4)
// If you select the already-used NRF_TIMER_0 or NRF_TIMER_2, it'll be auto modified to use NRF_TIMER_1

// For core mbed core 2.0.0-
// Depending on the board, you can select NRF52 Hardware Timer from NRF_TIMER_3,NRF_TIMER_4 (3 and 4)
// If you select the already-used NRF_TIMER_0, NRF_TIMER_1 or NRF_TIMER_2, it'll be auto modified to use NRF_TIMER_3

// Init NRF52 timer NRF_TIMER3
NRF52_MBED_Timer ITimer(NRF_TIMER_3);

#define HW_TIMER_INTERVAL_US          10000L      // 10ms

#define OUTPUTPIN         (12)  //  DigIO 12

void TimerHandler()
{
  static bool toggle      = false;

  digitalWrite(OUTPUTPIN, toggle);
  toggle = !toggle;
}

///////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////

void setup()
{
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(OUTPUTPIN, OUTPUT);

  Serial.begin(115200);
  while (!Serial);

  delay(100);

  Serial.print(F("\nStarting PinToggle on ")); Serial.println(BOARD_NAME);
  Serial.println(NRF52_MBED_TIMER_INTERRUPT_VERSION);

  // Interval in microsecs
  if (ITimer.attachInterruptInterval(HW_TIMER_INTERVAL_US, TimerHandler))
  {
    Serial.print(F("Starting ITimer OK, millis() = ")); Serial.println(millis());
  }
  else
    Serial.println(F("Can't set ITimer. Select another freq. or interval"));
}    

///////////////////////////////////////////////////////////////////////////

// For testing so that you know delay has no effect on the correctness of TimerInterrupt
#define BLOCKING_TIME_MS      3000L

void loop()
{
  // This unadvised blocking task is used to demonstrate the blocking effects onto the execution and accuracy to Software timer
  delay(BLOCKING_TIME_MS);
}

Can you provide a minimal example with the BLE setup and your periodic event which is being blocked.
If you are looking at the 10ms square wave on a scope, can you post an image of the effect on the waveform?

Thanks, but actually in the original code where millis() is polled in loop(), current time is incremented by the tick period in the loop every 10mS. In the other test approach using a Ticker callback, currentTime isn't used at all.

Thanks, I'm aiming to do all that today. At the moment I only have a simple test app (which works fine using any approach) that has no BLE code in it, and the full app (which doesn't work fine) which has all the BLE code. So I'm aiming to build up the test app with some simple BLE code to find out at what point it gets into trouble.

What I have noticed is that with the Ticker, if the tick rate is 1mS, then in the app counting 10 ticks to get the 10mS poll I need, it misses a lot more ticks than if I set it to a 10mS tick rate. Both go wrong, but the 1mS is worse. Not surprising, I suppose.

So I'm aiming to build up the test app with some simple BLE code to find out at what point it gets into trouble.

I would focus on using the code from @khoih-prog using
#include "NRF52_MBED_TimerInterrupt.h" instead of the Ticker.

He has given you code which uses Timer3 and is independent from the timer running BLE.

He is the guru on the hardware timers of the Nano33 BLE, and you have a better chance of success following his timer interrupt methods. If you can document an issue with BLE and his timer interrupts using Timer3 he will weigh in.

Might have been wrong ....

I've modified the test app in various ways so with #ifdefs I can toggle between using millis() and the Ticker, change the Ticker from 10x 1mS ticks and 1x a 10mS tick, and initialise or not BLE. Also, can change the cycle of output pulses from 1 high / 1 low to 1 high / 'n' low. The latter allows me to look at the accumulated overall time error over - say - 0.5 secs on a scope.

Firstly, regarding BLE, by literally just publishing a single BLE service (no characteristics), that is enough to make it lose ticks when using millis(). Not a lot but when the overall cycle time is 0.5 secs I can see slight random cycle period changes.

Switch to the Ticker, and even at 1mS tick rate counted 10x, never saw an error, ditto at 10mS tick rate.

So, maybe it is OK and I was wrong about Ticker.

Going now to move the 'long' output cycle code to the real BLE app which has a number of characteristics and does things with them, input and output. That is the real thing, so variable output cycle times would be an issue there. I will report back.

I did have a quick look at NRF52_MBED_TimerInterrupt yesterday, but ran into an instant problem where on installing the library it failed to find an included header file - cannot recall the name but it was something like "tinyusb.h". I didn't pursue it any further at the time because was looking at the Ticker, so uninstalled it again.

So, if the Ticker experiments with the real code fail - still get accumulated errors, I'll go back to looking at NRF52_MBED_TimerInterrupt.

Perhaps I should say I'm using Visual Studio Code with PlatformIO as the development environment, not the Arduino IDE - it is a more comprehensive environment and I use VSC for other things anyway (Marlin builds, Unity game code etc). Don't know if that will complicate using the timer library - it did find and install it OK, but there was the header file issue. I have had problems in the past with VSC Intellisense not finding things that I have never got to the bottom of.

It's good to hear that you are having success with Ticker.

I have been testing using Ticker in a BLE application with a simple integer characteristic and the Ticker blinking an led at 5 hz and I can see no disruptions in the blink pattern but I certainly have not been looking at minor variations in a timing routine.

One test might be to update and .writeValue() a millis() or micros() timestamp in an unsigned long characteristic every 10 ms with Ticker.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.