npio
Version:
High performance GPIO/i2c/PWM/SPI module for Raspberry Pi, NanoPi and Orange Pi
888 lines (757 loc) • 20.4 kB
JavaScript
/*
* Copyright (c) 2015 Jonathan Perkin <jonathan@perkin.org.uk>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
var binding = require('bindings')('npio');
var fs = require('fs');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
/*
* Event Emitter gloop.
*/
function npio() {
EventEmitter.call(this);
}
util.inherits(npio, EventEmitter);
module.exports = new npio;
/*
* Constants.
*/
npio.prototype.LOW = 0x0;
npio.prototype.HIGH = 0x1;
/*
* Supported function select modes. INPUT and OUTPUT match the bcm2835
* function select integers. PWM is handled specially.
*/
npio.prototype.INPUT = 0x0;
npio.prototype.OUTPUT = 0x1;
npio.prototype.PWM = 0x2;
/*
* Configure builtin pullup/pulldown resistors.
*/
npio.prototype.PULL_OFF = 0x0;
npio.prototype.PULL_DOWN = 0x1;
npio.prototype.PULL_UP = 0x2;
/*
* Pin edge detect events. Default to both.
*/
npio.prototype.POLL_LOW = 0x1; /* Falling edge detect */
npio.prototype.POLL_HIGH = 0x2; /* Rising edge detect */
npio.prototype.POLL_BOTH = 0x3; /* POLL_LOW | POLL_HIGH */
/*
* Reset pin status on close (default), or preserve current status.
*/
npio.prototype.PIN_PRESERVE = 0x0;
npio.prototype.PIN_RESET = 0x1;
/*
* GPIO Pad Control
*/
npio.prototype.PAD_GROUP_0_27 = 0x0;
npio.prototype.PAD_GROUP_28_45 = 0x1;
npio.prototype.PAD_GROUP_46_53 = 0x2;
npio.prototype.PAD_DRIVE_2mA = 0x00;
npio.prototype.PAD_DRIVE_4mA = 0x01;
npio.prototype.PAD_DRIVE_6mA = 0x02;
npio.prototype.PAD_DRIVE_8mA = 0x03;
npio.prototype.PAD_DRIVE_10mA = 0x04;
npio.prototype.PAD_DRIVE_12mA = 0x05;
npio.prototype.PAD_DRIVE_14mA = 0x06;
npio.prototype.PAD_DRIVE_16mA = 0x07;
npio.prototype.PAD_HYSTERESIS = 0x08;
npio.prototype.PAD_SLEW_UNLIMITED = 0x10;
/*
* Default pin mode is 'physical'. Other option is 'gpio'
*/
var npio_inited = false;
var npio_options = {
gpiomem: true,
mapping: 'physical',
mock: false,
};
/* Default mock mode if hardware is unsupported. */
var defmock = "raspi-3";
/*
* Wrapper functions.
*/
function bindcall(bindfunc, optarg) {
if (npio_options.mock)
return;
return bindfunc(optarg);
}
function bindcall2(bindfunc, arg1, arg2) {
if (npio_options.mock)
return;
return bindfunc(arg1, arg2);
}
function bindcall3(bindfunc, arg1, arg2, arg3) {
if (npio_options.mock)
return;
return bindfunc(arg1, arg2, arg3);
}
function warn(msg) {
console.error("WARNING: " + msg);
}
/*
* Map physical pin to BCM GPIOxx numbering. There are currently three
* layouts:
*
* PINMAP_26_R1: 26-pin early original models A and B (PCB rev 1.0)
* PINMAP_26: 26-pin standard models A and B (PCB rev 2.0)
* PINMAP_40: 40-pin models
*
* A -1 indicates an unusable pin. Each table starts with a -1 so that we
* can index into the array by pin number.
*/
var pincache = {};
var pinmap = null;
var chipType = null;
var pinmaps = {
/*
* Original Raspberry Pi, PCB revision 1.0
*/
PINMAP_26_R1: [
-1,
-1, -1, /* P1 P2 */
0, -1, /* P3 P4 */
1, -1, /* P5 P6 */
4, 14, /* P7 P8 */
-1, 15, /* P9 P10 */
17, 18, /* P11 P12 */
21, -1, /* P13 P14 */
22, 23, /* P15 P16 */
-1, 24, /* P17 P18 */
10, -1, /* P19 P20 */
9, 25, /* P21 P22 */
11, 8, /* P23 P24 */
-1, 7 /* P25 P26 */
],
/*
* Original Raspberry Pi, PCB revision 2.0.
*
* Differs to R1 on pins 3, 5, and 13.
*
* XXX: no support yet for the P5 header pins.
*/
PINMAP_26: [
-1,
-1, -1, /* P1 P2 */
2, -1, /* P3 P4 */
3, -1, /* P5 P6 */
4, 14, /* P7 P8 */
-1, 15, /* P9 P10 */
17, 18, /* P11 P12 */
27, -1, /* P13 P14 */
22, 23, /* P15 P16 */
-1, 24, /* P17 P18 */
10, -1, /* P19 P20 */
9, 25, /* P21 P22 */
11, 8, /* P23 P24 */
-1, 7 /* P25 P26 */
],
/*
* Raspberry Pi 40-pin models.
*
* First 26 pins are the same as PINMAP_26.
*/
PINMAP_40: [
-1,
-1, -1, /* P1 P2 */
2, -1, /* P3 P4 */
3, -1, /* P5 P6 */
4, 14, /* P7 P8 */
-1, 15, /* P9 P10 */
17, 18, /* P11 P12 */
27, -1, /* P13 P14 */
22, 23, /* P15 P16 */
-1, 24, /* P17 P18 */
10, -1, /* P19 P20 */
9, 25, /* P21 P22 */
11, 8, /* P23 P24 */
-1, 7, /* P25 P26 */
0, 1, /* P27 P28 */
5, -1, /* P29 P30 */
6, 12, /* P31 P32 */
13, -1, /* P33 P34 */
19, 16, /* P35 P36 */
26, 20, /* P37 P38 */
-1, 21 /* P39 P40 */
],
PINMAP_ORANGEPI: [
-1,
-1, -1,
12, -1,
11, -1,
6, 13,
-1, 14,
1, 110,
0, -1,
3, 68,
-1, 71,
64, -1,
65, 2,
66, 67,
-1, 21,
19, 18,
7, -1,
8, 200,
9, -1,
10, 201,
20, 198,
-1, 199
],
PINMAP_NANOPI_M1_PLUS: [
-1,
-1, -1,
12, -1,
11, -1,
203, 198,
-1, 199,
0, 6,
2, -1,
3, 200,
-1, 201,
64, -1,
65, 1,
66, 67,
-1, 17,
19, 18,
20, -1,
21, -1,
-1, -1,
-1, -1,
9, -1,
-1, -1
],
PINMAP_NEO: [
-1,
-1, -1,
12, -1,
11, -1,
203, 198,
-1, 199,
0, 6,
2, -1,
3, 200,
-1, 201,
64, -1,
65, 1,
66, 67,
-1, 17,
19, 18,
20, -1,
21, 7,
8, -1,
16, 13,
9,15,
-1, 14
],
PINMAP_NEO2: [
-1,
-1, -1,
12, -1,
11, -1,
203, 198,
-1, 199,
0, 6,
2, -1,
3, 200,
-1, 201,
64, -1,
65, 1,
66, 77
]
};
/*
* Pin event polling. We track which pins are being monitored, and create a
* bitmask for efficient checks. The event_poll function is executed in a
* setInterval() context whenever any pins are being monitored, and emits
* events when their EDS bit is set.
*/
var event_pins = {};
var event_mask = 0x0;
var event_running = false;
function event_poll() {
var active = bindcall(binding.gpio_event_poll, event_mask);
for (var gpiopin in event_pins) {
if (active & (1 << gpiopin))
module.exports.emit('pin' + gpiopin);
}
}
function read(path) {
try {
var contents = fs.readFileSync(path, 'ascii');
if (contents) {
return contents.toString();
}
} catch (err) {
/* It is the purpose of this function to silently catch this error */
}
return false;
}
/*
* Set up GPIO mapping based on board revision.
*/
function detect_pinmap() {
var cpuinfo, boardrev, match, cpu, mips;
cpuinfo = read('/proc/cpuinfo');
if (!cpuinfo) return false;
cpuinfo.split(/\n/).forEach(function(line) {
if (match = line.match(/^Revision.*(.{4})/)) {
boardrev = parseInt(match[1], 16);
} else if (match = line.match(/^Hardware.*:\s([^\s]+)/)) {
cpu = match[1];
} else if (match = line.match(/^BogoMIPS.*:\s([^\s]+)/)) {
mips = +match[1];
}
});
switch (boardrev) {
case 0x2:
case 0x3:
pinmap = "PINMAP_26_R1";
chipType = "BCM2835";
break;
case 0x4:
case 0x5:
case 0x6:
case 0x7:
case 0x8:
case 0x9:
case 0xd:
case 0xe:
case 0xf:
pinmap = "PINMAP_26";
chipType = "BCM2835";
break;
case 0x10:
case 0x12:
case 0x13:
case 0x15:
case 0x92:
case 0x93:
case 0xc1:
case 0x1041:
case 0x2042:
case 0x2082:
pinmap = "PINMAP_40";
chipType = "BCM2835";
break;
default:
switch (cpu) {
case 'Allwinner':
{
var model = read('/proc/device-tree/model');
switch (model) {
// Not well tested. Working on my device running Armbian
case 'FriendlyElec NanoPi M1 Plus\u0000':
chipType = "SUNXI";
pinmap = "PINMAP_NANOPI_M1_PLUS";
break;
case 'FriendlyARM NanoPi NEO\u0000':
chipType = "SUNXI";
pinmap = "PINMAP_NEO";
break;
default:
return false;
}
}
break;
case 'Allwinnersun50iw2Family':
//Allwinner H5 CPU
pinmap = "PINMAP_NEO2";
chipType = "SUNXI";
break;
case 'sun50i':
case 'sun8i':
//Allwinner H3 CPU
chipType = "SUNXI";
if (mips > 1000) {
pinmap = "PINMAP_ORANGEPI";
} else {
pinmap = "PINMAP_NEO";
}
break;
default:
return false;
}
}
return true;
}
function set_mock_pinmap() {
switch (npio_options.mock) {
case 'raspi-b-r1':
pinmap = "PINMAP_26_R1";
chipType = "BCM2835";
break;
case 'raspi-a':
case 'raspi-b':
pinmap = "PINMAP_26";
chipType = "BCM2835";
break;
case 'raspi-a+':
case 'raspi-b+':
case 'raspi-2':
case 'raspi-3':
case 'raspi-zero':
case 'raspi-zero-w':
pinmap = "PINMAP_40";
chipType = "BCM2835";
break;
default:
return false;
}
return true;
}
function pin_to_gpio(pin) {
if (pincache[pin])
return pincache[pin];
switch (npio_options.mapping) {
case 'physical':
if (pinmaps[pinmap][pin] == -1 || pinmaps[pinmap][pin] == null)
throw new Error("Invalid pin: physical=" + pin);
pincache[pin] = pinmaps[pinmap][pin];
break;
case 'gpio':
if (pinmaps[pinmap].indexOf(pin) === -1)
throw new Error("Invalid pin: gpio=" + pin);
pincache[pin] = pin;
break;
default:
throw new Error("Unsupported GPIO mode");
}
return pincache[pin];
}
function check_sys_gpio(pin) {
if (chipType === "BCM2835" && fs.existsSync('/sys/class/gpio/gpio' + pin))
throw new Error("GPIO" + pin + " is currently in use by /sys/class/gpio");
}
function get_pwm_function(pin) {
var gpiopin = pin_to_gpio(pin);
switch (gpiopin) {
case 12:
case 13:
return 4; /* BCM2835_GPIO_FSEL_ALT0 */
case 18:
case 19:
return 2; /* BCM2835_GPIO_FSEL_ALT5 */
default:
throw new Error("Pin " + pin + " does not support hardware PWM");
}
}
function get_pwm_channel(pin) {
var gpiopin = pin_to_gpio(pin);
switch (gpiopin) {
case 12:
case 18:
return 0;
case 13:
case 19:
return 1;
default:
throw new Error("Unknown PWM channel for pin " + pin);
}
}
function set_pin_pwm(pin) {
var gpiopin = pin_to_gpio(pin);
var channel, func;
if (npio_options.gpiomem)
throw new Error("PWM not available in gpiomem mode");
check_sys_gpio(gpiopin);
/*
* PWM channels and alternate functions differ from pin to pin, set
* them up appropriately based on selected pin.
*/
channel = get_pwm_channel(pin);
func = get_pwm_function(pin);
bindcall2(binding.gpio_function, gpiopin, func);
/*
* For now we assume mark-space mode, balanced is unsupported.
*/
bindcall3(binding.pwm_set_mode, channel, 1, 1);
}
/*
* GPIO
*/
/*
* Default warning handler, if the user registers their own then this one
* is cancelled.
*/
function default_warn_handler(msg) {
if (module.exports.listenerCount('warn') > 1) {
module.exports.removeListener('warn', default_warn_handler);
return;
}
warn(msg);
}
module.exports.on('warn', default_warn_handler);
npio.prototype.init = function(opts) {
opts = opts || {};
for (var k in npio_options) {
if (k in opts)
npio_options[k] = opts[k];
}
/*
* Invalidate the pin cache and mapping as we may be in the process
* of changing them.
*/
pincache = {};
pinmap = null;
chipType = null;
/*
* Allow the user to specify a mock board to emulate, otherwise try
* to autodetect the board, and fall back to mock mode if running on
* an unsupported platform.
*/
if (npio_options.mock) {
if (!set_mock_pinmap())
throw new Error("Unsupported mock mode " + npio_options.mock);
} else {
if (!detect_pinmap()) {
module.exports.emit('warn',
'Hardware auto-detect failed, running in ' +
defmock + ' mock mode');
npio_options.mock = defmock;
set_mock_pinmap();
}
}
/*
* init npio library
*/
var gpiomem = Number(npio_options.gpiomem);
if (chipType === "SUNXI") gpiomem += 2;
bindcall(binding.npio_init, gpiomem);
npio_inited = true;
};
npio.prototype.open = function(pin, mode, init) {
if (!npio_inited) {
/* PWM requires full /dev/mem */
if (mode === npio.prototype.PWM)
npio_options.gpiomem = false;
npio.prototype.init();
}
var gpiopin = pin_to_gpio(pin);
check_sys_gpio(gpiopin);
switch (mode) {
case npio.prototype.INPUT:
if (init !== undefined)
bindcall2(binding.gpio_pud, gpiopin, init);
return bindcall2(binding.gpio_function, gpiopin, npio.prototype.INPUT);
case npio.prototype.OUTPUT:
if (init !== undefined)
bindcall2(binding.gpio_write, gpiopin, init);
return bindcall2(binding.gpio_function, gpiopin, npio.prototype.OUTPUT);
case npio.prototype.PWM:
return set_pin_pwm(pin);
default:
throw new Error("Unsupported mode " + mode);
}
};
npio.prototype.mode = function(pin, mode) {
var gpiopin = pin_to_gpio(pin);
switch (mode) {
case npio.prototype.INPUT:
return bindcall2(binding.gpio_function, gpiopin, npio.prototype.INPUT);
case npio.prototype.OUTPUT:
return bindcall2(binding.gpio_function, gpiopin, npio.prototype.OUTPUT);
case npio.prototype.PWM:
return set_pin_pwm(pin);
default:
throw new Error("Unsupported mode " + mode);
}
};
npio.prototype.read = function(pin) {
return bindcall(binding.gpio_read, pin_to_gpio(pin));
};
npio.prototype.readbuf = function(pin, buf, len) {
if (len === undefined)
len = buf.length;
if (len > buf.length)
throw new Error("Buffer not large enough to accommodate request");
return bindcall3(binding.gpio_readbuf, pin_to_gpio(pin), buf, len);
};
npio.prototype.write = function(pin, value) {
return bindcall2(binding.gpio_write, pin_to_gpio(pin), value);
};
npio.prototype.writebuf = function(pin, buf, len) {
if (len === undefined)
len = buf.length;
if (len > buf.length)
throw new Error("Buffer not large enough to accommodate request");
return bindcall3(binding.gpio_writebuf, pin_to_gpio(pin), buf, len);
};
npio.prototype.readpad = function(group) {
if (npio_options.gpiomem)
throw new Error("Pad control not available in gpiomem mode");
return bindcall(binding.gpio_pad_read, group);
};
npio.prototype.writepad = function(group, control) {
if (npio_options.gpiomem)
throw new Error("Pad control not available in gpiomem mode");
bindcall2(binding.gpio_pad_write, group, control);
};
npio.prototype.pud = function(pin, state) {
bindcall2(binding.gpio_pud, pin_to_gpio(pin), state);
};
npio.prototype.poll = function(pin, cb, direction) {
var gpiopin = pin_to_gpio(pin);
if (direction === undefined)
direction = npio.prototype.POLL_BOTH;
/*
* If callback is a function, set up pin for polling, otherwise
* clear it.
*/
if (typeof(cb) === 'function') {
if (gpiopin in event_pins)
throw new Error("Pin " + pin + " is already listening for events.");
bindcall2(binding.gpio_event_set, gpiopin, direction);
var pincb = function() {
cb(pin);
};
module.exports.on('pin' + gpiopin, pincb);
event_pins[gpiopin] = pincb;
event_mask |= (1 << gpiopin);
if (!(event_running))
event_running = setInterval(event_poll, 1);
} else {
if (!(gpiopin in event_pins))
throw new Error("Pin " + pin + " is not listening for events.");
bindcall(binding.gpio_event_clear, gpiopin);
npio.prototype.removeListener('pin' + gpiopin, event_pins[gpiopin]);
delete event_pins[gpiopin];
event_mask &= ~(1 << gpiopin);
if (Object.keys(event_pins).length === 0) {
clearInterval(event_running);
event_running = false;
}
}
};
npio.prototype.close = function(pin, reset) {
var gpiopin = pin_to_gpio(pin);
if (reset === undefined)
reset = npio.prototype.PIN_RESET;
if (gpiopin in event_pins)
npio.prototype.poll(pin, null);
if (reset) {
if (!npio_options.gpiomem)
npio.prototype.pud(pin, npio.prototype.PULL_OFF);
npio.prototype.mode(pin, npio.prototype.INPUT);
}
};
/*
* PWM
*/
npio.prototype.pwmSetClockDivider = function(divider) {
if (divider !== 0 && (divider & (divider - 1)) !== 0)
throw new Error("Clock divider must be zero or power of two");
return bindcall(binding.pwm_set_clock, divider);
};
npio.prototype.pwmSetRange = function(pin, range) {
var channel = get_pwm_channel(pin);
return bindcall2(binding.pwm_set_range, channel, range);
};
npio.prototype.pwmSetData = function(pin, data) {
var channel = get_pwm_channel(pin);
return bindcall2(binding.pwm_set_data, channel, data);
};
/*
* i²c
*/
npio.prototype.i2cBegin = function() {
if (!npio_inited) {
/* i²c requires full /dev/mem */
npio_options.gpiomem = false;
npio.prototype.init();
}
if (npio_options.gpiomem)
throw new Error("i²c not available in gpiomem mode");
bindcall(binding.i2c_begin);
};
npio.prototype.i2cSetSlaveAddress = function(addr) {
return bindcall(binding.i2c_set_slave_address, addr);
};
npio.prototype.i2cSetClockDivider = function(divider) {
if ((divider % 2) !== 0)
throw new Error("Clock divider must be an even number");
return bindcall(binding.i2c_set_clock_divider, divider);
};
npio.prototype.i2cSetBaudRate = function(baud) {
return bindcall(binding.i2c_set_baudrate, baud);
};
npio.prototype.i2cRead = function(buf, len) {
if (len === undefined)
len = buf.length;
if (len > buf.length)
throw new Error("Buffer not large enough to accommodate request");
return bindcall2(binding.i2c_read, buf, len);
};
npio.prototype.i2cWrite = function(buf, len) {
if (len === undefined)
len = buf.length;
if (len > buf.length)
throw new Error("Buffer not large enough to accommodate request");
return bindcall2(binding.i2c_write, buf, len);
};
npio.prototype.i2cEnd = function() {
bindcall(binding.i2c_end);
};
/*
* SPI
*/
npio.prototype.spiBegin = function() {
if (!npio_inited) {
/* SPI requires full /dev/mem */
npio_options.gpiomem = false;
npio.prototype.init();
}
if (npio_options.gpiomem)
throw new Error("SPI not available in gpiomem mode");
bindcall(binding.spi_begin);
};
npio.prototype.spiChipSelect = function(cs) {
return bindcall(binding.spi_chip_select, cs);
};
npio.prototype.spiSetCSPolarity = function(cs, active) {
return bindcall2(binding.spi_set_cs_polarity, cs, active);
};
npio.prototype.spiSetClockDivider = function(divider) {
if ((divider % 2) !== 0 || divider < 0 || divider > 65536)
throw new Error("Clock divider must be an even number between 0 and 65536");
return bindcall(binding.spi_set_clock_divider, divider);
};
npio.prototype.spiSetDataMode = function(mode) {
return bindcall(binding.spi_set_data_mode, mode);
};
npio.prototype.spiTransfer = function(txbuf, rxbuf, len) {
return bindcall3(binding.spi_transfer, txbuf, rxbuf, len);
};
npio.prototype.spiWrite = function(buf, len) {
return bindcall2(binding.spi_write, buf, len);
};
npio.prototype.spiEnd = function() {
bindcall(binding.spi_end);
};
/*
* Misc functions.
*/
npio.prototype.sleep = function(secs) {
bindcall(binding.npio_usleep, secs * 1000000);
};
npio.prototype.msleep = function(msecs) {
bindcall(binding.npio_usleep, msecs * 1000);
};
npio.prototype.usleep = function(usecs) {
bindcall(binding.npio_usleep, usecs);
};
process.on('exit', function(code) {
bindcall(binding.npio_close);
});