This discussion has been locked.
You can no longer post new replies to this discussion. If you have a question you can start a new discussion

Servo PPM Routines for Nano 33 BLE (Examples & Questions)

Good evening, (I have a few questions which are at the bottom, so I hope you don't mind me having posted the full length of these two programs first)

I've managed to produce some great results in developing PPM (pulse position modulation; related to PWM pulse width modulation) for a head tracker project I'm working on. I've included two fundamentally different programs as described below, which I'm testing using my Nano 33 BLE via the Arduino development environment...

Method 1: Servo PPM using 2 timers (4 channels)

This method produces the best results (although method 2 does indeed work quite well). The servos move virtually perfectly smooth almost 100% of the time. Timer 3 CC[0] sets the initial high value of the waveform, followed by CC[1] to [4] for the 4 channels. CC[5] is used for the PPM frame length which is set to 22500 microseconds. Each timer 3 compare activates timer 4 which in turn controls the pulse length.

The GPIOTE tasks are controlled by timer events via PPI channels. One complete frame length is processed by timer 3 independent of timer 3 IRQ handler. After the frame is complete, timer 3 stops and clears, then the new servo values are transferred, and the timer is started again. The delay until the next frame starts is dependent on timer 3 prescaler which is set to 1MHz, which could be increased, and the PPM channel values scaled accordingly.

Note: I've omitted all Timer "_Pos" instructions except for TIMER_INTENSET (for GPIOTE I decided to just leave them all in place). _Pos is required for INTENSET even though it has only one field in its register (note "Write 1 to enable..." in the description. Without _Pos it doesn't work). Although SHORTS has 2 fields in its register, it doesn't need _Pos because I'm using its 1st field.  

#include <nrf.h>

#define PPI_CHANNEL_0 (0)
#define PPI_CHANNEL_1 (1)
#define PPI_CHANNEL_2 (2)
#define PPI_CHANNEL_3 (3)
#define PPI_CHANNEL_4 (4)
#define PPI_CHANNEL_5 (5)	

// #define PIN_GPIO (16) // Green for Nano 33 BLE
#define PIN_GPIO (2) 
#define PORT (1)

/* Using timer 1 via MBED doesn't work, so perhaps it's being used */

void setup()
{
	// Configure PIN_GPIO as output
	// ----------------------------
	NRF_GPIO->DIRSET = (1UL << PIN_GPIO);
	
	// Configure GPIOTE
	// ----------------
	NRF_GPIOTE->CONFIG[0] =
	
	(GPIOTE_CONFIG_MODE_Task    << GPIOTE_CONFIG_MODE_Pos) |		
	(PORT                	    << GPIOTE_CONFIG_PORT_Pos) |	
	(PIN_GPIO                   << GPIOTE_CONFIG_PSEL_Pos);		
	
	// Configure TIMER3 & TIMER4 to generate EVENTS_COMPARE[n]
	// -------------------------------------------------------
	NRF_TIMER3->BITMODE = TIMER_BITMODE_BITMODE_32Bit;			
	NRF_TIMER3->PRESCALER = 4;			
	NRF_TIMER3->CC[0] = 1000; // Values [0] to [4] set here can be anything because they're set in TIMER3_IRQHandler_v 
	NRF_TIMER3->CC[1] = 2500;	
	NRF_TIMER3->CC[2] = 4000;	
	NRF_TIMER3->CC[3] = 5500;
	NRF_TIMER3->CC[4] = 7000;
	NRF_TIMER3->CC[5] = 22500; // PPM Frame length
	
	NRF_TIMER3->INTENSET = TIMER_INTENSET_COMPARE5_Enabled << TIMER_INTENSET_COMPARE5_Pos; 
	NVIC_EnableIRQ(TIMER3_IRQn); 		
	
	NRF_TIMER3->TASKS_START = 1;		
	
	// Pulse length timer
	// ------------------
	NRF_TIMER4->MODE = TIMER_MODE_MODE_Timer;
	NRF_TIMER4->BITMODE = TIMER_BITMODE_BITMODE_32Bit;
	NRF_TIMER4->SHORTS = TIMER_SHORTS_COMPARE0_CLEAR_Enabled;
	NRF_TIMER4->PRESCALER = 4; // 1MHz
	NRF_TIMER4->CC[0] = 500; // Pulse length	
	
	// Configure PPI channels with connection between TIMER->EVENTS_COMPARE[n] and GPIOTE->TASKS_SET[0]
	// ------------------------------------------------------------------------------------------------
	NRF_PPI->CH[PPI_CHANNEL_0].EEP = (uint32_t) & NRF_TIMER3->EVENTS_COMPARE[0];
	NRF_PPI->CH[PPI_CHANNEL_0].TEP = (uint32_t) & NRF_GPIOTE->TASKS_SET[0];		
	NRF_PPI->FORK[PPI_CHANNEL_0].TEP = (uint32_t) & NRF_TIMER4->TASKS_START;
	
	NRF_PPI->CH[PPI_CHANNEL_1].EEP = (uint32_t) & NRF_TIMER3->EVENTS_COMPARE[1];
	NRF_PPI->CH[PPI_CHANNEL_1].TEP = (uint32_t) & NRF_GPIOTE->TASKS_SET[0];		
	NRF_PPI->FORK[PPI_CHANNEL_1].TEP = (uint32_t) & NRF_TIMER4->TASKS_START;
	
	NRF_PPI->CH[PPI_CHANNEL_2].EEP = (uint32_t) & NRF_TIMER3->EVENTS_COMPARE[2];
	NRF_PPI->CH[PPI_CHANNEL_2].TEP = (uint32_t) & NRF_GPIOTE->TASKS_SET[0];		
	NRF_PPI->FORK[PPI_CHANNEL_2].TEP = (uint32_t) & NRF_TIMER4->TASKS_START;
	
	NRF_PPI->CH[PPI_CHANNEL_3].EEP = (uint32_t) & NRF_TIMER3->EVENTS_COMPARE[3];
	NRF_PPI->CH[PPI_CHANNEL_3].TEP = (uint32_t) & NRF_GPIOTE->TASKS_SET[0];		
	NRF_PPI->FORK[PPI_CHANNEL_3].TEP = (uint32_t) & NRF_TIMER4->TASKS_START;
	
	NRF_PPI->CH[PPI_CHANNEL_4].EEP = (uint32_t) & NRF_TIMER3->EVENTS_COMPARE[4];
	NRF_PPI->CH[PPI_CHANNEL_4].TEP = (uint32_t) & NRF_GPIOTE->TASKS_SET[0];		
	NRF_PPI->FORK[PPI_CHANNEL_4].TEP = (uint32_t) & NRF_TIMER4->TASKS_START;	
	
	// --------------------------------------------------------------------
	
	NRF_PPI->CH[PPI_CHANNEL_5].EEP = (uint32_t) & NRF_TIMER4->EVENTS_COMPARE[0]; // Control pulse length
	NRF_PPI->CH[PPI_CHANNEL_5].TEP = (uint32_t) & NRF_GPIOTE->TASKS_CLR[0];		
	NRF_PPI->FORK[PPI_CHANNEL_5].TEP = (uint32_t) & NRF_TIMER4->TASKS_STOP;				
	
	// Enable PPI channels
	// -------------------
	NRF_PPI->CHENSET = (1UL << PPI_CHANNEL_0);
	NRF_PPI->CHENSET = (1UL << PPI_CHANNEL_1);
	NRF_PPI->CHENSET = (1UL << PPI_CHANNEL_2);
	NRF_PPI->CHENSET = (1UL << PPI_CHANNEL_3);
	NRF_PPI->CHENSET = (1UL << PPI_CHANNEL_4);
	NRF_PPI->CHENSET = (1UL << PPI_CHANNEL_5);		
}

volatile int initial_off_period = 1000; // The off period will always remain the same, therefore this initial value is arbitrary
volatile int chan1 = 1500;
volatile int chan2 = 1500;
volatile int chan3 = 1500;
volatile int chan4 = 1500;

void loop()
{			 
	while (1)
	{
		// __WFE();			
	}
}

extern "C" void TIMER3_IRQHandler_v (void)
{		
	// volatile uint32_t dummy;
	
	static int travel_rate = 5;
	
	if (NRF_TIMER3->EVENTS_COMPARE[5] == 1)
	{
		NRF_TIMER3->EVENTS_COMPARE[5] = 0;		
		NRF_TIMER3->TASKS_STOP = 1;		
		NRF_TIMER3->TASKS_CLEAR = 1;		
		
		// Read back event register to ensure we have cleared it before exiting IRQ handler
		// dummy = NRF_TIMER3->EVENTS_COMPARE[5];
		// dummy; // To get rid of set but not used warning	
		
		chan1 += travel_rate;
		chan2 += travel_rate;
		chan3 += travel_rate;
		chan4 += travel_rate;
		
		if (chan1 > 1900)
		{
			chan1 = 950;
			chan2 = 950;
			chan3 = 950;
			chan4 = 950;
		} 
		
		NRF_TIMER3->CC[0] = initial_off_period;
		NRF_TIMER3->CC[1] = NRF_TIMER3->CC[0] + chan1;
		NRF_TIMER3->CC[2] = NRF_TIMER3->CC[1] + chan2;
		NRF_TIMER3->CC[3] = NRF_TIMER3->CC[2] + chan3;
		NRF_TIMER3->CC[4] = NRF_TIMER3->CC[3] + chan4;
		
		NRF_TIMER3->TASKS_START = 1;			
	}
}

Method 2: Servo PPM using 1 timer (8 channels)

Unlike method 1, the entire PPM waveform is constructed inside the timer handler and therefore is dependent on its timing accuracy/consistency. You can have as many channels as PPM supports, even though only one timer is being used. I've added a demo option which requires setting the "demo_multiplier" value and "sigPin" to an LED.

Synchronisation seems like a key point. For example, by considering method 1, it's clear that all 4 channel values are transferred whilst the timer is stopped. However for this single timer method, the channel values are transferred without stopping the timer. **Refer to the 5th post for clarification about this** 

// The following program is adapted from here: https://forum.arduino.cc/t/mbed-os-isr-linkage-on-arduino-33-ble/898811 and here: https://quadmeup.com/generate-ppm-signal-with-arduino
// -------------------------------------------

#include <nrf_timer.h>

NRF_TIMER_Type* timer = NRF_TIMER4;
IRQn_Type timer_irq = TIMER4_IRQn;

#define NUMBER_OF_CHANNELS 8 		// Number of channels		
#define CHANNEL_DEFAULT_VALUE 950	// Default servo value
#define FRAME_LENGTH 22500			// PPM frame length in microseconds (1ms = 1000µs)
#define PULSE_LENGTH 500  			// Pulse length		 

#define sigPin 10 // 23 is the Nano 33 BLE Green LED. 10 is ~D10... See here for pin definitions: https://github.com/arduino/ArduinoCore-nRF528x-mbedos/blob/master/variants/ARDUINO_NANO33BLE/pins_arduino.h#L54		

volatile int demo_multiplier = 1; // 200 is good for testing via the onboard LEDs (Don't forget to change "sigPin" above)	
volatile int ppm [NUMBER_OF_CHANNELS];

extern "C" void TIMER4_IRQHandler_v()
{		
	static int cur_chan_numb;
	static unsigned int calc_rest;
	
	static bool state = true;
	
	if (timer->EVENTS_COMPARE[0] == 1)
	{   
		timer->EVENTS_COMPARE[0] = 0;							
		
		if (state) // Start pulse
		{  					
			digitalWrite (sigPin, HIGH); // On state				
			timer->CC[0] = PULSE_LENGTH * demo_multiplier;
			
			state = false;
		}  
		else // End pulse and calculate when to start the next pulse
		{  			
			state = true;					
			
			digitalWrite (sigPin, LOW); // Off state
			
			if (cur_chan_numb >= NUMBER_OF_CHANNELS)
			{
				cur_chan_numb = 0;					
				calc_rest = calc_rest + PULSE_LENGTH;  					
				timer->CC[0] = (FRAME_LENGTH - calc_rest) * demo_multiplier;				
				calc_rest = 0;
			}
			else
			{					
				timer->CC[0] = (ppm[cur_chan_numb] - PULSE_LENGTH) * demo_multiplier;					
				calc_rest = calc_rest + ppm[cur_chan_numb];					
				cur_chan_numb++;
			}     
		}
	}			
}

void setupTimer (NRF_TIMER_Type* timer, unsigned int timer_period)
{	
	timer->MODE = NRF_TIMER_MODE_TIMER;
	timer->BITMODE = TIMER_BITMODE_BITMODE_32Bit;
	timer->SHORTS = TIMER_SHORTS_COMPARE0_CLEAR_Enabled;		
	timer->PRESCALER = NRF_TIMER_FREQ_1MHz;
	timer->CC[0] = timer_period;
	
	timer->INTENSET = TIMER_INTENSET_COMPARE0_Enabled << TIMER_INTENSET_COMPARE0_Pos;	
	NVIC_EnableIRQ (timer_irq);
	
	timer->TASKS_START = 1;
}

void setup()
{		
	pinMode (sigPin, OUTPUT);
	
	setupTimer (timer, 10000); // Setup timer registers and start timer. Note that this value here is quite arbitrary because it's set in TIMER4_IRQHandler_v()	
	
	for (int i = 0; i < NUMBER_OF_CHANNELS; ++i)		
	    // ppm[i] = CHANNEL_DEFAULT_VALUE;
    	ppm[i] = 950 + i * 100; // This produces a varied but consistent offset LED demo via demo_multiplier
}

float currentTime = 0;
float previousTime = 0;
float deltaTime = 0; 

void loop()
{
	if (demo_multiplier == 1) // Don't run here for LED demo
	{
		currentTime = micros();
		deltaTime += currentTime - previousTime;
		previousTime = currentTime;          
		
		if (deltaTime > 10000) // 100Hz
		{
			deltaTime = 0;
			
			for (int i = 0; i < NUMBER_OF_CHANNELS; ++i)
			    if (ppm[i] > 1900)
			        ppm[i] = 950;
			    else
			        ppm[i] += 3;
		} 
	}
}

Here are my questions

  1. When are timer CC values updated if the transfer is initiated whilst the timer is running? 
  2. Commented in timer 3 handler for method 1 is "dummy" from here Nordic Timer Example. I thought that "NRF_TIMER3->EVENTS_COMPARE[n] = 0" took care of that, so please kindly explain to me what the two dummy lines do?
  3. How important is __WFE() and what does it do? (I have it commented, and everything seems fine)
  4. With respect to the findings as detailed in my "Note:" near the beginning of this post in relation to INTENSET for Timer, please explain why _Pos is required even though INTENSET's register only has one field?
  5. Please advise of any improvements for either method? Although both work well, method 1 as I already said is very smooth but limited to 4 channels via two timers, unless for example reusing the CC values by using a timer handler via EVENTS_COMPARE[4] to stop it, transfer the next set of PPM values, and restart again. Like it's already doing but two stages instead of one, but that would of course introduce a timer handler interruption part way in constructing the waveform.  
Related