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
- When are timer CC values updated if the transfer is initiated whilst the timer is running?
- 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?
- How important is __WFE() and what does it do? (I have it commented, and everything seems fine)
- 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?
- 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.