Sometimes you run into an application that requires something to happen in real-time. I’m not talking about seconds or milliseconds, we’re talking about microsecond accuracy here.
In this post, you’ll learn how to use some hidden functionality Particle Mesh.
Hidden functionality you say?
Yes, it’s buried deep within Device OS calling your name!
By the end of this post, you’ll be able to run real time tasks without getting bogged down. That’s right, we’re going to decouple the processor from the equation. That way you can run your Mesh application with confidence!
Let’s get started!
Hidden Features
The smart folks at Nordic knew they had a problem on their hands. On one hand, adding amazing radio technology like 802.15.4 was changing the industry. On the other, it was harder and harder to process and generate signals with high precision.
In response to this, they created a special network inside their chips. It’s purpose was to allow peripherals to directly speak with other. If an event happens in one place, it will trigger a change in the other immediately. There’s no slowdown or intervention from the processor.
What is this magical system?
It’s called PPI.
PPI stands for Programmable Peripheral Interface. It’s used to connect events of one peripheral to “tasks” of another peripheral. This event and task structure allows you to run an application and real-time tasks at the same time.
PPI sounds amazing right? It does come with one drawback, it’s hard to understand at first glance. What’s considered a task? What’s considered an event? How the heck do you set it up in the first place?
Have no fear, real world example is here. 😎👍
Real World Example
RS-485 is a standard that allows you to transmit serial data over a differential twisted pair. While it has many uses, one of them is to control stage lighting. Yup, if you’ve ever been to a rock show, they’re likely powered by RS-485 in one way or another.
RS-485 though requires a protocol and DMX happens to be that protocol. DMX operates at 250k baud and 8 data bits, no parity and 2 stop bits. That is straight forward. The not so straightforward parts is the “break” signal.
After doing some research I had found that the break signal was a high to low transition, a hold of 88µS and then back to high. Some Arduino DMX implementations used a baud rate of 83333, and a 0x00 byte to send this signal. While this is crafty, the NRF52 inside Particle Mesh doesn’t support this baud rate.
Cue the sad trombone. ☹️
There were some solutions that came to mind though. Here’s a summary of what worked and didn’t work.
Side note: here’s a great resource on DMX timing (and everything low level DMX). It’s a great resource especially if you’re doing something “unconventional” with DMX.
Solution 1: Timer & ISR
One way to to attempt to generate the break signal would be to use the timer to generate an ISR. That ISR would fire at the beginning of the 88µS and at the end. All we’d have to do was control a GPIO. Easy peasy right?
First we need a timer to make everything work. According to Andrey (Firmware Guru @ Particle) the timer configuration on Particle Mesh is:
Timer0: Softdevice (No way Jose, can’t touch this guy) Timer1: Radio (Not uh. Can’t touch this one either) Timer2: Serial1 (Used by UART on the TX/RX pins. Could potentially use it but it would require sharing between the Timer/PPI and UART) Timer3: Serial2 (Used for the LTE radio or Wifi for Boron and Argon respectively) Timer4: NFC (Only needed if you’re using NFC)
So, Timer 3 or Timer 4 were the best choices.
Next, it was time to configure the timer. This part does require some NRF52 SDK calls that are normally not exposed as C++ wiring.
The Code
Here’s the beginning of the initialization code for the timer. We’ll be using the timer so make sure to include nrfx_timer.h
at the top of your application code:
#include "nrfx_timer.h"
nrfx_timer_t timer4 = NRFX_TIMER_INSTANCE(4);
We’ll also include timer4
which is pointing to the TIMER4 instance.
Next, let’s configure some things. Place it in setup()
:
// Setup for timer control
attachInterruptDirect(TIMER4_IRQn,nrfx_timer_4_irq_handler);
// Timer configuration
nrfx_timer_config_t timer_config = NRFX_TIMER_DEFAULT_CONFIG;
// Set priority as high as possible.
timer_config.interrupt_priority = 3;
// Init the timer
uint32_t err_code = nrfx_timer_init(&timer4,&timer_config,timerEventHandler);
if( err_code != NRF_SUCCESS ) Log.error("nrfx_timer_error");
// Disable and clear the timer.
nrfx_timer_disable(&timer4);
nrfx_timer_clear(&timer4);
attachInterruptDirect
allows the user code to attach directly to the Timer4 interrupt handler. If you don’t run this, your code will likely cause an assert and the red blink of death.
NRFX_TIMER_DEFAULT_CONFIG
is sufficient enough for this application. The only thing to change was the priority. The soft device reserves anything below 3. Thus 3 the highest priority we could use without encroaching on soft device land.
nrfx_timer_init
is our main timer init function. You pass in a pointer to the timer, config and also the interrupt function. Where the interrupt function looks something like this:
// High speed timer event handler!
void timerEventHandler(nrf_timer_event_t event_type, void * p_context) {
if( event_type == NRF_TIMER_EVENT_COMPARE0) {
pinResetFast(D9);
if ( event_type == NRF_TIMER_EVENT_COMPARE1 ) {
pinSetFast(D9);
dmxReadyEvent = true;
}
}
Side note: pinResetFast
and pinSetFast
are inline functions that execute fast. Without using these there would have been even more of a delay.
So, we have some initialization code, and we have an event handler, now what?
It’s time to configure the timer compare channels. 🎉
// Arbitrary offset to start at
uint32_t offset = 1000;
// Calculate the ticks for 88 uS
uint32_t ticks = nrfx_timer_us_to_ticks(&timer4,88);
// Set the compare for the start and the end
nrfx_timer_compare(&timer4, NRF_TIMER_CC_CHANNEL0, offset, true);
nrfx_timer_compare(&timer4, NRF_TIMER_CC_CHANNEL1, ticks+offset, true);
offset
can be set to any value really. Anything greater than 0 that way you know the first comparison will fire.
nrfx_timer_us_to_tick
is to get the timer tick count from a microsecond value. You can also calculate this manually:
The clock frequency is 16Mhz. There is no pre-scaler or divider. Thus every lock tick is about 62.5ns. Divide that 88µS by 62.5ns and you’ll get 1408 ticks. If you looked at this variable with the debugger you’d find that it has the same value. Excellent!
Finally we use nrfx_timer_compare
to set up two comparison events. The first when we get to offset
and the second when we get to offset+ticks
.
If you look back at the timerEventHandler
you can see we handle both the COMPARE0 and the COMPARE1 event. Those events get fired because of our use of nrfx_timer_compare
We’ve configured timer and interrupt! Now, start everything with nrfx_timer_enable(&timer4);
(setup()
is a good place for this at first)
Did it work?
Sorta.
In some cases the timing was very accurate. In some cases not at all. This is because these signals are at the whim of how busy the processor is. The more busy, the more likely that this signal will be delayed. Here’s a capture of the “88µS pulse” when Mesh is active:
As you can imagine, if there’s a bunch more going on inside your device, this get’s much much worse. Let’s fix this issue by introducing PPI! In the next section you’ll learn how to use PPI to generate our 88µS pulse.
Solution 2: PPI, Timer and GPIOTE
PPI at first glance looks foreign. (It sure was to me when I first played with it!) By the end of this example, you should have a good grasp of the different components. That way you can go ahead and play with it more on your own!
The Code
We’re going to build off the code we tried above. First we’ll want to include both nrfx_ppi.h
and nrfx_gpiote.h
#undef CHG
#include "nrfx_timer.h"
#include "nrfx_ppi.h"
#include "nrfx_gpiote.h"
const uint8_t tx_pin = 6;
Side note: when you include “nrfx_ppi.h” is has a conflict with PPI’s CHG
register. A quick solution is to use #undef
to remove the CHG
reference created in pinmap_defines.h
Another side note: tx_pin
is used because the pin numbers used for Particle do not match to Nordic’s built in functions. In this case pin D9
is pin 6 on the NRF52840
Just below that we’ll include our PPI channel definitions (you can put it right above timer4
):
nrf_ppi_channel_t compare0,compare1;
Then, in setup
after our tick calculation, we’ll change some things and add some things.
// Set the compare for the start and the end
nrfx_timer_compare(&timer4, NRF_TIMER_CC_CHANNEL0, offset, false);
nrfx_timer_compare(&timer4, NRF_TIMER_CC_CHANNEL1, ticks+offset, false);
// Setup GPIOTE
nrfx_gpiote_init();
nrfx_gpiote_out_config_t gpiote_cfg;
gpiote_cfg.init_state = NRF_GPIOTE_INITIAL_VALUE_HIGH;
gpiote_cfg.task_pin = true;
gpiote_cfg.action = NRF_GPIOTE_POLARITY_TOGGLE;
err_code = nrfx_gpiote_out_init(tx_pin, &gpiote_cfg);
// Allocate PPI channels
err_code = nrfx_ppi_channel_alloc(&compare0);
err_code = nrfx_ppi_channel_alloc(&compare1);
// Assign events to the PPI channels
err_code = nrfx_ppi_channel_assign(compare0,
nrfx_timer_event_address_get(&timer4,nrf_timer_compare_event_get(NRF_TIMER_CC_CHANNEL0)),
nrfx_gpiote_clr_task_addr_get(tx_pin));
err_code = nrfx_ppi_channel_assign(compare1,
nrfx_timer_event_address_get(&timer4,nrf_timer_compare_event_get(NRF_TIMER_CC_CHANNEL1)),
nrfx_gpiote_set_task_addr_get(tx_pin));
// Enable the PPI channels
nrfx_ppi_channel_enable(compare0);
nrfx_ppi_channel_enable(compare1);
For both nrfx_timer_compare
, notice how the last parameters were changed to false
? This is to disable calls to timerEventHandler
.
We then initialize the gpiote
peripheral. We’ll want to use NRF_GPIOTE_POLARITY_TOGGLE
so we can bring the GPIO high and low.
We allocate both PPI channels using nrfx_ppi_channel_alloc
and then assign them.
Here comes the tricky part. 😬
With nrfx_ppi_channel_assign
you need to assign an event and a task. This was confusing for me so it will likely be confusing for you. The first argument is the PPI channel. Next is the event that will cause the PPI to forward to another peripheral to complete a task.
We’ll use nrfx_timer_event_address_get
to get the address of the compare channel 0 of timer 4. As a reminder, this gets triggered after offset
.
We want to set the GPIO low, so we’ll use nrfx_gpiote_clr_task_addr_get
. The argument of this is the NRF52 GPIO pin. Which, in our case, is tx_pin
.
We’ll do the same for the compare1
channel. The only difference being we’ll be using NRF_TIMER_CC_CHANNEL1
and nrfx_gpiote_set_task_addr_get
. That way the GPIO gets set back high once 88µS have elapsed.
The last things we have to do is enable the GPIOTE task and then start the timer!
// Enable the task
nrfx_gpiote_out_task_enable(6);
// Enable the timer
nrfx_timer_enable(&timer4);
Side note: this will continuously run until you disable the timer. I disable the timer using the timerEventHandler
using nrfx_timer_disable(&timer4)
and nrfx_timer_clear(&timer4)
;
Did it work?
Yup.
While more complicated, the break signal using PPI was far more accurate. Here’s what it looks like with Mesh connected on infinite persistence.
Woo. 🎊
As expected, we get a nice consistent 88µS pulse no matter what. No processor involved == accurate timing!
Conclusion
The PPI peripheral can be handy when processor time is at a premium. You’ve seen the advantages of PPI over traditional interrupt based timing. (and how PPI beat the socks off interrupt case every time!) You can start pushing the boundaries of your applications now that you have examples! It’s surprising what you can do with these little things! All it takes is some creative thinking. 😉
If you’ve enjoyed this, make sure you subscribe to my list below. You’ll also get updates about my upcoming guide on Particle Mesh. I’d also like thank you for reading, If you have any comments or questions leave them here or send me an email.
Until next time!
Example Code
Full working code is located here.
Last Modified: 2020.3.7