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.