nRF52 Low Level Interface Library

The nRF52 microcontroller used in Puck.js, Pixl.js and MDBT42Q has a load of really interesting peripherals built-in, not all of which are exposed by Espruino. The microcontroller also contains something called PPI - the "Programmable Peripheral Interconnect". This allows you to 'wire' peripherals together internally.

PPI lets you connect an event (eg. a pin changing state) to a task (eg. increment the counter). All of this is done without the processor being involved, allowing for very fast and also very power efficient peripheral use.

Check out the chip's reference manual for more information.

This library (NRF52LL (About Modules)) provides a low level interface to PPI and some of the nRF52's peripherals.

Note: Failure to 'shut down' peripherals when not in use could drastically increase the nRF52's power consumption.

Basic Usage

  • Initialise a peripheral to create events
  • Initialise a peripheral you want to send tasks to
  • Set up and Enable a PPI to wire the two together

The following are some examples:

Count the number of times the BTN pin changes state

Uses GPIO and counter timer:

var ll = require("NRF52LL");
// Source of events - the button
var btn = ll.gpiote(7, {type:"event",pin:BTN,lo2hi:1,hi2lo:1});
// A place to recieve Tasks - a counter
var ctr = ll.timer(3,{type:"counter"});
// Set up and enable PPI
ll.ppiEnable(0, btn.eIn, ctr.tCount);
/* This function triggers a Task by hand to 'capture' the counter's
value. It can then be read back from the relevant `cc` register */
function getCtr() {
  poke32(ctr.tCapture[0],1);
  return peek32(ctr.cc[0]);
}

Create a square wave on pin D0, with the inverse of the square wave on D1

Uses GPIO and counter timer:

var ll = require("NRF52LL");
// set up D0 and D1 as outputs
digitalWrite(D0,0);
digitalWrite(D1,0);
// create two 'toggle' tasks, one for each pin
var t0 = ll.gpiote(7, {type:"task",pin:D0,lo2hi:1,hi2lo:1,initialState:0});
var t1 = ll.gpiote(6, {type:"task",pin:D1,lo2hi:1,hi2lo:1,initialState:1});
// create a timer that counts up to 1000 and back at full speed
var tmr = ll.timer(3,{cc:[1000],cc0clear:1});
// use two PPI to trigger toggle events
ll.ppiEnable(0, tmr.eCompare[0], t0.tOut);
ll.ppiEnable(1, tmr.eCompare[0], t1.tOut);
// Manually trigger a task to start the timer
poke32(tmr.tStart,1);

Toggle LED every time D31's analog value goes above VCC/2

Uses low power comparator + GPIO:

var ll = require("NRF52LL");
// set up LED as an output
digitalWrite(LED,0);
// create a 'toggle' task for the LED
var tog = ll.gpiote(7, {type:"task",pin:LED,lo2hi:1,hi2lo:1,initialState:0});
// compare D31 against vref/2
var comp = ll.lpcomp({pin:D31,vref:8});
// use a PPI to trigger the toggle event
ll.ppiEnable(0, comp.eCross, tog.tOut);

Note: As of Espruino 2v25 you can set up the comparator to create a JS event with E.setComparator

Count how many times D31 crosses VCC/2 in 10 seconds

Uses low power comparator + counter timer:

var ll = require("NRF52LL");
// source of events - compare D31 against vref/2
var comp = ll.lpcomp({pin:D31,vref:8});
// A place to recieve events - a counter
var ctr = ll.timer(3,{type:"counter"});
// Set up and enable PPI
ll.ppiEnable(0, comp.eCross, ctr.tCount);
/* This function triggers a Task by hand to 'capture' the counter's value. It can then clear it and read back the relevant `cc` register */
function getCtr() {
  poke32(ctr.tCapture[0],1);
  poke32(ctr.tClear,1); // reset it
  return peek32(ctr.cc[0]);
}
// Every 10 seconds, wake and print out the number of crosses
setInterval(function() {
  print(getCtr());
}, 10000);

Note: As of Espruino 2v25 you can set up the comparator to create a JS event with E.setComparator

Use LED1 on Puck.js to sense a change in light level

LED1 in Puck.js can be a light sensor, and we can use the low power comparator with this to detect a state change.

To make this work we have to use one IO pin (in this case D1) so that we can toggle it with each change, and then watch it with setWatch for changes.

Note: On the MBDT42 breakout board, LED1 isn't attached to an analog pin so this won't work. However LED2 is, so can still be used in this way.

var ll = require("NRF52LL");
var togglePin = D1;
analogRead(LED1);
digitalWrite(togglePin,0);
// create a 'toggle' task for togglePin
var tog = ll.gpiote(7, {type:"task",pin:togglePin,lo2hi:1,hi2lo:1,initialState:0});
// compare LED1 against 3/16 vref (vref is in 1/16 ths)
var comp = ll.lpcomp({pin:LED1,vref:3,hyst:true});
// use a PPI to trigger the toggle event
ll.ppiEnable(0, comp.eCross, tog.tOut);

// Detect a change on togglePin
setWatch(function() {
  // called twice per 'flash' (for light on and off)
  print("Light level changed");
}, togglePin, {repeat:true});

Note: As of Espruino 2v25 you can set up the comparator to create a JS event with E.setComparator

Make one reading from the ADC:

Uses the ADC (much line analogRead but with more options)

var ll = require("NRF52LL");
var saadc = ll.saadc({
  channels : [ { // channel 0
    pin:D31,
    gain:1/4,
    tacq:40,
    refvdd:true,
  } ]
});
print(saadc.sample()[0]);
saadc.stop(); // deconfigure so analogRead works again (use saadc.start() to redo)

Make a differential from the ADC:

Use the ADC to measure the voltage difference between D30 and D31, with the maximum gain and oversampling provided by the hardware.

var ll = require("NRF52LL");
var saadc = ll.saadc({
  channels : [ { // channel 0
    pin:D30, npin:D31,
    gain:4,
    tacq:40,
    refvdd:true,
  } ],
  oversample : 8
});
print(saadc.sample()[0]);
saadc.stop(); // deconfigure so analogRead works again (use saadc.start() to redo)

Read a buffer of data from the ADC

Uses ADC.

It's also possible to use .sample(...) for this, but this example shows you how to use it in more detail.

The ADC will automatically sample at the given sample rate.

var ll = require("NRF52LL");
// Buffer to fill with data
var buf = new Int16Array(128);
// source of events - compare D31 against vref/2
var saadc = ll.saadc({
  channels : [ { // channel 0
    pin:D31,
    gain:1/4,
    tacq:40,
    refvdd:true,
  } ],
  samplerate:2047, // 16Mhz / 2047 = 7816 Hz auto-sampling
  dma:{ptr:E.getAddressOf(buf,true), cnt:buf.length},
});
// Start sampling until the buffer is full
poke32(saadc.eEnd,0); // clear flag so we can test
poke32(saadc.tStart,1);
poke32(saadc.tSample,1); // start!
while (!peek32(saadc.eEnd)); // wait until it ends
poke32(saadc.tStop,1);
print("Done!", buf);
saadc.stop(); // deconfigure so analogRead works again (use saadc.start() to redo)

Read a buffer of data from the ADC, alternating between 2 pins

Uses ADC and counter timer.

The NRF52 doesn't support using samplerate (as in the last example) with more than one channel, so you have to use another timer to trigger the tSample task.

var ll = require("NRF52LL");
// Buffer to fill with data
var buf = new Int16Array(128);
// ADC
var saadc = ll.saadc({
  channels : [ {
    pin:D31, // channel 0
    gain:1/4,
    refvdd:true
  }, {
    pin:D30, // channel 1
    gain:1/4,
    refvdd:true
  } ],
  dma:{ptr:E.getAddressOf(buf,true), cnt:buf.length},
});
// create a timer that counts up to 1000 and back at full speed
var tmr = ll.timer(3,{cc:[1000],cc0clear:1});
// use two PPI to trigger toggle events
ll.ppiEnable(0, tmr.eCompare[0], saadc.tSample);
// Start sampling until the buffer is full
poke32(saadc.eEnd,0); // clear flag so we can test
poke32(saadc.tStart,1);
// start the timer
poke32(tmr.tStart,1);
while (!peek32(saadc.eEnd)); // wait until sampling ends
poke32(tmr.tStop,1);
poke32(saadc.tStop,1);
print("Done!", buf);
saadc.stop(); // deconfigure so analogRead works again (use saadc.start() to redo)

Use the RTC to toggle the state of a LED

Uses RTC, GPIO:

var ll = require("NRF52LL");

// set up LED as an output
digitalWrite(LED,0);
// create a 'toggle' task for the LED
var tog = ll.gpiote(7, {type:"task",pin:LED,lo2hi:1,hi2lo:1,initialState:0});

// set up the rtc
var rtc = ll.rtc(2);
poke32(rtc.prescaler, 4095); // 32kHz / 4095 = 8 Hz
rtc.enableEvent("eTick");
poke32(rtc.tStart,1); // start RTC
// use a PPI to trigger the toggle event
ll.ppiEnable(0, rtc.eTick, tog.tOut);

Use the RTC to measure how long a button has been held down for:

Uses RTC, GPIO:

var ll = require("NRF52LL");
// Source of events - the button
// Note: this depends on the polarity of the physical button (this assumes that 0=pressed)
var btnu = ll.gpiote(7, {type:"event",pin:BTN,lo2hi:1,hi2lo:0});
var btnd = ll.gpiote(6, {type:"event",pin:BTN,lo2hi:0,hi2lo:1});
// A place to recieve Tasks - the RTC
var rtc = ll.rtc(2);
poke32(rtc.prescaler, 0); // no prescaler, 32 kHz
poke32(rtc.tStop, 1); // ensure RTC is stopped
// Set up and enable PPI to start and stop the RTC
ll.ppiEnable(0, btnd.eIn, rtc.tStart);
ll.ppiEnable(1, btnu.eIn, rtc.tStop);
// Every so often, check the RTC and report the result
setInterval(function() {
  print(peek32(rtc.counter));
  poke32(rtc.tClear, 1);
}, 5000);

Hardware capacitive sense on two pins

Uses GPIO, counter timer:

Note: the counter timer has 6 capture/compare registers. We use 1 to produce the PWM and 2 for the two capacitive sense pins - the remaining 3 could be used for 3 more capacitive sense lines.

// connect one 100k resistor between PINDRV and PIN1
// and one 100k resistor between PINDRV and PIN2
function capSense2(PINDRV, PIN1, PIN2) {
  var ll = require("NRF52LL");
  digitalWrite(PINDRV,0);
  digitalRead([PIN1,PIN2]);
  // create a 'toggle' task for output
  var t0 = ll.gpiote(7, {type:"task",pin:PINDRV,lo2hi:1,hi2lo:1,initialState:0});
  // two input tasks, one for each cap sense input
  var e1 = ll.gpiote(6, {type:"event",pin:PIN1,lo2hi:1,hi2lo:0});
  var e2 = ll.gpiote(5, {type:"event",pin:PIN2,lo2hi:1,hi2lo:0});
  // create a timer that counts up to 1000 and back at full speed
  var tmr = ll.timer(3,{cc:[1000],cc0clear:1});
  // use a PPI to trigger toggle events
  ll.ppiEnable(0, tmr.eCompare[0], t0.tOut);
  // use 2 more to 'capture' the current timer value when a pin changes from low to high
  ll.ppiEnable(1, e1.eIn, tmr.tCapture[1]);
  ll.ppiEnable(2, e2.eIn, tmr.tCapture[2]);
  // Manually trigger a task to clear and start the timer
  poke32(tmr.cc[0],0); // compare with 0 for PWM
  poke32(tmr.tClear,1);
  poke32(tmr.tStart,1);
  return { read : function() {
    return [ peek32(tmr.cc[1]), peek32(tmr.cc[2]) ];
  } };
}

var cap = capSense2(D25, D31, D5);

setInterval(function() {
  console.log(cap.read());
},500);

Reference

/* GPIO Tasks and Events
  ch can be between 0 and 7 for low power GPIO. setWatch uses GPIOTEs internally
  (starting from 0), so it's a good idea to start from GPIOTE 7 and work down to
  avoid conflicts.

  opts is {
    type : "event"/"task"/"disabled", // default is disabled
    pin : D0, // pin number to use,
    lo2hi : 0/1, // default 0, trigger on low-high transition
    hi2lo : 0/1, // default 0, trigger on high-low transition
    initialState : 0, // default 0, initial pin state when toggling states
  }
  returns {
    config,// address of config register
    tOut,  // task to set/toggle pin (lo2hi/hi2lo)
    tSet,  // task to set pin to 1
    tClr,  // task to set pin to 0
    eIn    // event when GPIO changes
  }
*/
exports.gpiote = function (ch, opts) { ... }

/* 32 bit timers
  ch can be 0..4
  opts is {
    mode : "counter"/"timer", // default = timer
    bits : 8/16/24/32, // default = 32
    prescaler : 0..9, // default = 0
    cc : [5,6,7,8,9,10], // 6 (or less) capture/compare regs
    cc0clear : true, // if cc[0] matches, clear the timer
    cc3stop : true, // if cc[0] matches, stop the timer
    // for cc0..cc5
  };
  returns {
      shorts, // address of shortcut register
      mode, // address of mode register
      bitmode, // address of bitmode
      prescaler, // address of prescaler register
      cc, // array of 6 addresses of capture/compare registers
      tStart, // address of START task
      tStop, // address of STOP task
      tCount, // address of COUNT task
      tClear, // address of CLEAR task
      tShutdown, // address of SHUTDOWN task
      tCapture, // array of 6 addresses of capture tasks
      eCompare, // array of 6 addresses of compare events
  }
*/
exports.timer = function (ch, opts) { ... }

/* Low power comparator
  ch can be between 0 and 7 for low power GPIO
  opts is {
    pin : D0, // pin number to use,
    vref : 1, // reference voltage in 16ths of VDD (1..15), or D2/D3 to use those analog inputs
    hyst : true/false, // enable ~50mV hysteresis
  }
  returns { // addresses of
    result,  // comparator result
    enable,  // enable
    psel,    // pin select
    refsel,  // reference voltage
    extrefsel, // external reference select
    hyst,      // 50mv hysteresis
    shorts,  // shortcut register
    tStart,  //
    tStop,   // task to set pin to 1
    tSample, // task to set pin to 0
    eReady,  // sample ready
    eDown,eUp,eCross, // events for crossing
    sample() // samples the comparator and returns the result (0/1)
    cross() // return {up:bool,down:bool,cross:bool} since last cal;
  }

**Note:** As of Espruino 2v25 you can set up the comparator to create a JS event with E.setComparator()
*/
exports.lpcomp = function (opts) { ... }

/* Successive approximation analog-to-digital converter
  opts is {
    channels : [ {
      pin : D0, // pin number to use,
      npin : D1, // pin to use for negative input (or undefined)
      pinpull : undefined / "down" / "up" / "vcc/2",   // pin internal resistor state
      npinpull : undefined / "down" / "up" / "vcc/2",  // npin internal resistor state
      gain : 1/6, 1/5, 1/4, 1/3, 1/2, 1(default), 2, 4, // input gain -
      refvdd : bool, // use VDD/4 as a reference (true) or internal 0.6v ref (false)
      tacq : 3(default),5,10,15,20,40 // acquisition time in us
    }, { ... } ] // up to 8 channels
    resolution : 8,10,12,14 // bits (14 default)
    oversample : 0..8, // take 2<<X samples, 0(default) is just 1 sample. Can't be used with >1 channel
    samplerate : 0(default), 80..2047 // Sample from sample task, or at 16MHz/X. Can't be used with >1 channel
    dma : { ptr, cnt } // enable DMA. cnt is in 16 bit words
      // DMA IS REQUIRED UNLESS YOU'RE JUST USING THE 'sample' function
  }
  returns {
    status, // 0=ready, 1=busy
    config, // address of config register
    enable,
    amount, // words transferred since last tStart
    tStart,  // Start ADC
    tSample,  // Start sampling
    tStop,   // Stop ADC
    tCalib,  // Start calibration
    eEnd     // event when ADC has filled DMA buffer
    setDMA : function({ptr,cnt}) // set new DMA buffer
               // double-buffered so can be set again right after tStart
    start : function() // configure ADC (called automatically when 'saadc' created)
    sample : function(cnt) // Make `cnt*channels.length` readings and return the result. resets DMA
    stop : function() // uninitialise so that normal analogRead works
  }
*/
exports.saadc = function (opts) { ... }

/* Real time counter
  You should only set on ch2 as 0 and 1 are used by Espruino/Bluetooth
  This function intentionally doesn't set state itself to allow you
  to still query RTC0/1 without modifying them.

  returns {
    counter // address of 24 bit counter register
    prescaler : // address of 12 bit prescaler
    cc : [..] // addresses of 4 compare registers
    tStart // start counting
    tStop // stop counting
    tClear // clear counter
    tOverflow // set counter to 0xFFFFF0 to force overflow in the near future
    eTick : // the RTC has 'ticked' (see enableEvent)
    eOverflow : // RTC (see enableEvent)
    eCmp0 : // cc[0]==counter (see enableEvent)
    eCmp1 : // cc[1]==counter (see enableEvent)
    eCmp2 : // cc[2]==counter (see enableEvent)
    eCmp3 : // cc[3]==counter (see enableEvent)
    enableEvent : function(evt) // enable "eTick","eOverflow","eCmp0","eCmp1","eCmp2" or "eCmp3"
    disableEvent : function(evt) // disable enable "eTick","eOverflow","eCmp0","eCmp1","eCmp2" or "eCmp3"
  }
*/
exports.rtc = function (ch) { ... }

/* Set up and enable a PPI channel (0..15) - give it the address of the
event and task required
*/
exports.ppiEnable = function (ch, event, task) { ... }

// Disable a PPI channel
exports.ppiDisable = function (ch) { ... }

// Check if a PPI channel is enabled
exports.ppiIsEnabled = function (ch) { ... }

Interrupts

Espruino doesn't allow you to react to interrupts from the internal peripherals directly, however you can change the state of an external pin (see the examples above) and can then also use that as an input with setWatch.

'Use LED1 on Puck.js to sense a change in light level' above is a good example of that.

Note: setWatch uses a GPIOTE peripheral for each watch, starting with GPIOTE 0 - so be careful not to overlap them!

LPCOMP

LPCOMP is a low-power comparator. You can use it as follows:

var ll = require("NRF52LL");
// Compare D31 with 8/16 of vref (half voltage)
o = ll.lpcomp({pin:D31,vref:8});
// or {pin:D31,vref:D2} to compare with pin D2

// Read the current value of the comparator
console.log(o.sample());
// Return an object {up,down,cross} showing how
// the state changed since the last call
console.log(o.cross());
// eg { up: 1, down: 0, cross: 1 }

Note: As of Espruino 2v25 you can set up the comparator to create a JS event with E.setComparator

E.setComparator(D31, 8/16);
E.on("comparator", e => {
  // e==1 -> up
  // e==-1 -> down
});

This page is auto-generated from GitHub. If you see any mistakes or have suggestions, please let us know.