com.pi4j
pi4j-library-linuxfs
diff --git a/plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/LinuxFsPlugin.java b/plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/LinuxFsPlugin.java
index e4702db2..259d612a 100644
--- a/plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/LinuxFsPlugin.java
+++ b/plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/LinuxFsPlugin.java
@@ -37,6 +37,7 @@
import com.pi4j.plugin.linuxfs.provider.gpio.digital.LinuxFsDigitalOutputProvider;
import com.pi4j.plugin.linuxfs.provider.pwm.LinuxFsPwmProvider;
import com.pi4j.plugin.linuxfs.internal.LinuxPwm;
+import com.pi4j.plugin.linuxfs.provider.spi.LinuxFsSpiProvider;
import com.pi4j.provider.Provider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -100,10 +101,10 @@ public class LinuxFsPlugin implements Plugin {
public static final String I2C_PROVIDER_NAME = NAME + " I2C Provider";
public static final String I2C_PROVIDER_ID = ID + "-i2c";
-// // SPI Provider name and unique ID
-// public static final String SPI_PROVIDER_NAME = NAME + " SPI Provider";
-// public static final String SPI_PROVIDER_ID = ID + "-spi";
-//
+ // SPI Provider name and unique ID
+ public static final String SPI_PROVIDER_NAME = NAME + " SPI Provider";
+ public static final String SPI_PROVIDER_ID = ID + "-spi";
+
// // Serial Provider name and unique ID
// public static final String SERIAL_PROVIDER_NAME = NAME + " Serial Provider";
// public static final String SERIAL_PROVIDER_ID = ID + "-serial";
@@ -155,7 +156,8 @@ public void initialize(PluginService service) {
LinuxFsDigitalInputProvider.newInstance(gpioFileSystemPath),
LinuxFsDigitalOutputProvider.newInstance(gpioFileSystemPath),
LinuxFsPwmProvider.newInstance(pwmFileSystemPath, pwmChip),
- LinuxFsI2CProvider.newInstance()
+ LinuxFsI2CProvider.newInstance(),
+ LinuxFsSpiProvider.newInstance()
};
// register the LinuxFS I/O Providers with the plugin service
diff --git a/plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/internal/LinuxLibC.java b/plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/internal/LinuxLibC.java
new file mode 100644
index 00000000..a910cf64
--- /dev/null
+++ b/plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/internal/LinuxLibC.java
@@ -0,0 +1,59 @@
+package com.pi4j.plugin.linuxfs.internal;
+
+import com.sun.jna.Library;
+import com.sun.jna.Native;
+
+/**
+ * C library functions.
+ *
+ * @author mpilone
+ * @since 10/3/24.
+ */
+public interface LinuxLibC extends Library {
+
+ // This class could extend c.s.j.platform.linux.LibC, but we're not using any
+ // of that functionality right now so we can avoid the jna-platform dependency
+ // until we need it.
+
+ LinuxLibC INSTANCE = LinuxLibC.LibLoader.load();
+
+ class LibLoader {
+ static LinuxLibC load() {
+ return Native.load("c", LinuxLibC.class);
+ }
+ }
+
+ ///////////////////////////////////
+ // fcntl.h
+ int O_WRONLY = 00000001;
+ int O_RDWR = 00000002;
+ int O_NONBLOCK = 00004000;
+
+ ///////////////////////////////////
+ // ioctl.h
+ int _IOC_NRBITS = 8;
+ int _IOC_TYPEBITS = 8;
+ int _IOC_SIZEBITS = 14;
+
+ int _IOC_NRSHIFT = 0;
+ int _IOC_TYPESHIFT = (_IOC_NRSHIFT + _IOC_NRBITS);
+ int _IOC_SIZESHIFT = (_IOC_TYPESHIFT + _IOC_TYPEBITS);
+ int _IOC_DIRSHIFT = (_IOC_SIZESHIFT + _IOC_SIZEBITS);
+
+ byte _IOC_NONE = 0;
+ byte _IOC_WRITE = 1;
+ byte _IOC_READ = 2;
+
+ static int _IOC(byte dir, byte type, byte nr, int size) {
+ return (((dir) << _IOC_DIRSHIFT) |
+ ((type) << _IOC_TYPESHIFT) |
+ ((nr) << _IOC_NRSHIFT) |
+ ((size) << _IOC_SIZESHIFT));
+ }
+
+ int ioctl(int filedes, long op, Object... args);
+
+ int open(String pathname, int flags);
+
+ int close(int fd);
+}
diff --git a/plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/provider/spi/LinuxFsSpi.java b/plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/provider/spi/LinuxFsSpi.java
new file mode 100644
index 00000000..0d4a5223
--- /dev/null
+++ b/plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/provider/spi/LinuxFsSpi.java
@@ -0,0 +1,287 @@
+package com.pi4j.plugin.linuxfs.provider.spi;
+
+import com.pi4j.io.spi.Spi;
+import com.pi4j.io.spi.SpiBase;
+import com.pi4j.io.spi.SpiConfig;
+import com.pi4j.plugin.linuxfs.internal.LinuxLibC;
+import com.sun.jna.Memory;
+import com.sun.jna.Native;
+import com.sun.jna.Structure;
+import com.sun.jna.ptr.IntByReference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static com.pi4j.plugin.linuxfs.internal.LinuxLibC.*;
+
+/**
+ * SPI implementation that uses JNA bindings to the Linux SPI device (i.e. /dev/spidev0.0). Only supports writing but
+ * it works to drive an SSD1306 OLED display.
+ *
+ * @author mpilone
+ * @see
+ * spi-c-ioctl
+ * @see spidev.h
+ * @see spi.h
+ * @see spidev_fdx.c
+ * @see SPI userspace API
+ * @since 10/3/24.
+ */
+public class LinuxFsSpi extends SpiBase implements Spi {
+
+ private static final Logger LOG = LoggerFactory.getLogger(LinuxFsSpi.class);
+
+ ///////////////////////////////////
+ // From spidev.h
+ private final static byte SPI_IOC_MAGIC = 'k';
+ private final static byte SIZE_OF_BYTE = 1;
+ private final static byte SIZE_OF_INT = 4;
+
+ private static int SPI_IOC_MESSAGE(int n) {
+
+ // Even though we will pass the structure to ioctl as a pointer, the command needs to know
+ // the actual size of the structure (i.e. sizeof). Therefore, we use the ByValue interface
+ // when getting the struct size.
+ int structSize = Native.getNativeSize(spi_ioc_transfer.ByValue.class);
+ int msgSize = ((((n)*(structSize)) < (1 << _IOC_SIZEBITS))
+ ? ((n)*(structSize)) : 0);
+
+ return _IOC(_IOC_WRITE, SPI_IOC_MAGIC, (byte)0, msgSize);
+ }
+
+ // These could be replaced with the specific values generated from the _IOC method (a macro in the native C),
+ // but I think it is useful to see where the values come from.
+
+ /* Read / Write of SPI mode (SPI_MODE_0..SPI_MODE_3) (limited to 8 bits) */
+ private final static int SPI_IOC_RD_MODE = _IOC(_IOC_READ, SPI_IOC_MAGIC, (byte)1, SIZE_OF_BYTE);
+ private final static int SPI_IOC_WR_MODE = _IOC(_IOC_WRITE, SPI_IOC_MAGIC, (byte)1, SIZE_OF_BYTE);
+
+ /* Read / Write SPI bit justification */
+ private final static int SPI_IOC_RD_LSB_FIRST = _IOC(_IOC_READ, SPI_IOC_MAGIC, (byte)2, SIZE_OF_BYTE);
+ private final static int SPI_IOC_WR_LSB_FIRST = _IOC(_IOC_WRITE, SPI_IOC_MAGIC, (byte)2, SIZE_OF_BYTE);
+
+ /* Read / Write SPI device word length (1..N) */
+ private final static int SPI_IOC_RD_BITS_PER_WORD = _IOC(_IOC_READ, SPI_IOC_MAGIC, (byte)3, SIZE_OF_BYTE);
+ private final static int SPI_IOC_WR_BITS_PER_WORD = _IOC(_IOC_WRITE, SPI_IOC_MAGIC, (byte)3, SIZE_OF_BYTE);
+
+ /* Read / Write SPI device default max speed hz */
+ private final static int SPI_IOC_RD_MAX_SPEED_HZ = _IOC(_IOC_READ, SPI_IOC_MAGIC, (byte)4, SIZE_OF_INT);
+ private final static int SPI_IOC_WR_MAX_SPEED_HZ = _IOC(_IOC_WRITE, SPI_IOC_MAGIC, (byte)4, SIZE_OF_INT);
+
+ /* Read / Write of the SPI mode field */
+ private final static int SPI_IOC_RD_MODE32 = _IOC(_IOC_READ, SPI_IOC_MAGIC, (byte)5, SIZE_OF_INT);
+ private final static int SPI_IOC_WR_MODE32 = _IOC(_IOC_WRITE, SPI_IOC_MAGIC, (byte)5, SIZE_OF_INT);
+
+ @Structure.FieldOrder({"tx_buf", "rx_buf",
+ "len", "speed_hz",
+ "delay_usecs", "bits_per_word", "cs_change", "tx_nbits", "rx_nbits", "word_delay_usecs", "pad"})
+ public static class spi_ioc_transfer extends Structure {
+ public long tx_buf;
+ public long rx_buf;
+
+ public int len;
+ public int speed_hz;
+
+ public short delay_usecs;
+ public byte bits_per_word;
+ public byte cs_change;
+ public byte tx_nbits;
+ public byte rx_nbits;
+ public byte word_delay_usecs;
+ public byte pad;
+
+ public static class ByValue extends spi_ioc_transfer implements Structure.ByValue {}
+ }
+
+ ///////////////////////////////////
+ // spi.h
+ private final byte SPI_CPHA = 1; /* clock phase */
+ private final byte SPI_CPOL = 1 << 1; /* clock polarity */
+
+ private final byte SPI_MODE_0 = (0|0); /* (original MicroWire) */
+ private final byte SPI_MODE_1 = (0|SPI_CPHA);
+ private final byte SPI_MODE_2 = (SPI_CPOL|0);
+ private final byte SPI_MODE_3 = (SPI_CPOL|SPI_CPHA);
+
+ private final static String SPI_DEVICE_BASE = "/dev/spidev";
+ private final LinuxLibC libc = LinuxLibC.INSTANCE;
+ private int fd;
+
+ public LinuxFsSpi(LinuxFsSpiProviderImpl provider, SpiConfig config) {
+ super(provider, config);
+ }
+
+ @Override
+ public void open() {
+ super.open();
+
+ // From the docs (https://www.kernel.org/doc/html/v6.12/spi/spidev.html):
+ // For a SPI device with chipselect C on bus B, you should see:
+ //
+ // /dev/spidevB.C ...
+ // character special device, major number 153 with a dynamically chosen minor device number.
+ // This is the node that userspace programs will open, created by “udev” or “mdev”.
+ String spiDev = SPI_DEVICE_BASE + config().bus().getBus() + "." + config().getChipSelect().getChipSelect();
+ fd = libc.open(spiDev, LinuxLibC.O_RDWR);
+ if (fd < 0) {
+ throw new RuntimeException("Failed to open SPI device " + spiDev);
+ }
+
+ IntByReference intPtr = new IntByReference();
+ int ret = libc.ioctl(fd, SPI_IOC_RD_MODE32, intPtr);
+ if(ret != 0) {
+ libc.close(fd);
+ throw new RuntimeException("Could not read SPI mode.");
+ }
+
+ switch (config().mode()) {
+ case MODE_0:
+ intPtr.setValue(intPtr.getValue() | SPI_MODE_0);
+ break;
+ case MODE_1:
+ intPtr.setValue(intPtr.getValue() | SPI_MODE_1);
+ break;
+ case MODE_2:
+ intPtr.setValue(intPtr.getValue() | SPI_MODE_2);
+ break;
+ case MODE_3:
+ intPtr.setValue(intPtr.getValue() | SPI_MODE_3);
+ break;
+ }
+
+ ret = libc.ioctl(fd, SPI_IOC_WR_MODE32, intPtr);
+ if(ret != 0) {
+ libc.close(fd);
+ throw new RuntimeException("Could not write SPI mode..");
+ }
+
+ ret = libc.ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, intPtr);
+ if(ret != 0) {
+ libc.close(fd);
+ throw new RuntimeException("Could not read the SPI max speed.");
+ }
+
+ intPtr.setValue(config().baud());
+ ret = libc.ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, intPtr);
+ if(ret != 0) {
+ libc.close(fd);
+ throw new RuntimeException("Could not write the SPI max speed.");
+ }
+ }
+
+ @Override
+ public void close() {
+ libc.close(fd);
+
+ super.close();
+ }
+
+ @Override
+ public int transfer(byte[] write, int writeOffset, byte[] read, int readOffset, int numberOfBytes) {
+ PeerAccessibleMemory buf = new PeerAccessibleMemory(numberOfBytes);
+ buf.write(0, write, writeOffset, numberOfBytes);
+
+ // According to the docs you can use the same buffer for tx/rx.
+ spi_ioc_transfer transfer = new spi_ioc_transfer();
+ transfer.tx_buf = buf.getPeer();
+ transfer.rx_buf = buf.getPeer();
+ transfer.bits_per_word = 0;
+ transfer.speed_hz = config.baud();
+ transfer.delay_usecs = 0;
+ transfer.len = numberOfBytes;
+
+ int ret = libc.ioctl(fd, SPI_IOC_MESSAGE(1), transfer);
+ if (ret < 0) {
+ LOG.error("Could not write SPI message. ret {}, error: {}", ret, Native.getLastError());
+ numberOfBytes = -1;
+ }
+ else {
+ buf.read(0, read, readOffset, numberOfBytes);
+ }
+
+ buf.close();
+
+ return numberOfBytes;
+ }
+
+ @Override
+ public int read() {
+ byte[] buf = new byte[1];
+ if (read(buf, 0, 1) == 1) {
+ return buf[0];
+ }
+ else {
+ return -1;
+ }
+ }
+
+ @Override
+ public int read(byte[] buffer, int offset, int length) {
+ PeerAccessibleMemory buf = new PeerAccessibleMemory(length);
+
+ spi_ioc_transfer transfer = new spi_ioc_transfer();
+ transfer.tx_buf = 0;
+ transfer.rx_buf = buf.getPeer();
+ transfer.bits_per_word = 0;
+ transfer.speed_hz = config.baud();
+ transfer.delay_usecs = 0;
+ transfer.len = length;
+
+ int ret = libc.ioctl(fd, SPI_IOC_MESSAGE(1), transfer);
+ if (ret < 0) {
+ LOG.error("Could not write SPI message. ret {}, error: {}", ret, Native.getLastError());
+ length = -1;
+ }
+
+ buf.close();
+
+ return length;
+ }
+
+ @Override
+ public int write(byte b) {
+ return write(new byte[] {b}, 0, 1);
+ }
+
+ @Override
+ public int write(byte[] data, int offset, int length) {
+ // We could reuse this buffer for all requests as long as it is large enough.
+ // For now we'll alloc/free a new one on each request.
+ PeerAccessibleMemory buf = new PeerAccessibleMemory(length);
+ buf.write(0, data, offset, length);
+
+ spi_ioc_transfer transfer = new spi_ioc_transfer();
+ transfer.tx_buf = buf.getPeer();
+ transfer.rx_buf = 0;
+ transfer.bits_per_word = 0;
+ transfer.speed_hz = config.baud();
+ transfer.delay_usecs = 0;
+ transfer.len = length;
+
+ int ret = libc.ioctl(fd, SPI_IOC_MESSAGE(1), transfer);
+ if (ret < 0) {
+ LOG.error("Could not write SPI message. ret {}, error: {}", ret, Native.getLastError());
+ length = 0;
+ }
+
+ buf.close();
+
+ return length;
+ }
+
+ /**
+ * Extension of Memory to allow direct access to the native peer pointer. This is required because
+ * the {@link spi_ioc_transfer} structure uses a long for the tx_buf and rx_buf fields but a
+ * native pointer is normally only 32 bitsso we can't let JNA do the mapping for us. There may be a better
+ * way to do this with JNA. We fetch the peer and put it in a long in the struct to maintain proper struct
+ * alignment.
+ */
+ private static class PeerAccessibleMemory extends Memory {
+ public PeerAccessibleMemory(long size) {
+ super(size);
+ }
+
+ long getPeer() {
+ return peer;
+ }
+ }
+}
diff --git a/plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/provider/spi/LinuxFsSpiProvider.java b/plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/provider/spi/LinuxFsSpiProvider.java
new file mode 100644
index 00000000..ae50672b
--- /dev/null
+++ b/plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/provider/spi/LinuxFsSpiProvider.java
@@ -0,0 +1,28 @@
+package com.pi4j.plugin.linuxfs.provider.spi;
+
+import com.pi4j.io.spi.SpiProvider;
+import com.pi4j.plugin.linuxfs.LinuxFsPlugin;
+
+/**
+ * LinuxFsSpiProvider interface.
+ *
+ * @author mpilone
+ * @since 10/4/24.
+ */
+public interface LinuxFsSpiProvider extends SpiProvider {
+
+ /** {@link LinuxFsPlugin#SPI_PROVIDER_NAME} */
+ String NAME = LinuxFsPlugin.SPI_PROVIDER_NAME;
+ /** {@link LinuxFsPlugin#SPI_PROVIDER_ID} */
+ String ID = LinuxFsPlugin.SPI_PROVIDER_ID;
+
+ /**
+ * newInstance.
+ *
+ * @return a {@link LinuxFsSpiProviderImpl} object.
+ */
+ static LinuxFsSpiProviderImpl newInstance() {
+ return new LinuxFsSpiProviderImpl();
+ }
+
+}
diff --git a/plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/provider/spi/LinuxFsSpiProviderImpl.java b/plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/provider/spi/LinuxFsSpiProviderImpl.java
new file mode 100644
index 00000000..ed6d627c
--- /dev/null
+++ b/plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/provider/spi/LinuxFsSpiProviderImpl.java
@@ -0,0 +1,48 @@
+package com.pi4j.plugin.linuxfs.provider.spi;
+
+import com.pi4j.context.Context;
+import com.pi4j.exception.ShutdownException;
+import com.pi4j.io.spi.Spi;
+import com.pi4j.io.spi.SpiConfig;
+import com.pi4j.io.spi.SpiProvider;
+import com.pi4j.io.spi.SpiProviderBase;
+
+/**
+ * @author mpilone
+ * @since 10/3/24.
+ */
+public class LinuxFsSpiProviderImpl extends SpiProviderBase
+ implements LinuxFsSpiProvider {
+
+ public LinuxFsSpiProviderImpl() {
+ this.id = ID;
+ this.name = NAME;
+ }
+
+ @Override
+ public int getPriority() {
+ // the linux FS driver should always be higher priority
+ return 150;
+ }
+
+ @Override
+ public Spi create(SpiConfig config) {
+ Spi spi = new LinuxFsSpi(this, config);
+
+ // Is this the right place to call open? Should we have a shared spi device like the I2CBus?
+ spi.open();
+
+ this.context.registry().add(spi);
+ return spi;
+ }
+
+ @Override
+ public SpiProvider shutdown(Context context) throws ShutdownException {
+
+ // Is this the right place to call close?
+ this.context.registry().allByType(LinuxFsSpi.class).values()
+ .forEach(LinuxFsSpi::close);
+
+ return super.shutdown(context);
+ }
+}
diff --git a/plugins/pi4j-plugin-linuxfs/src/main/java/module-info.java b/plugins/pi4j-plugin-linuxfs/src/main/java/module-info.java
index 365fd0ee..85924c57 100644
--- a/plugins/pi4j-plugin-linuxfs/src/main/java/module-info.java
+++ b/plugins/pi4j-plugin-linuxfs/src/main/java/module-info.java
@@ -34,6 +34,7 @@
requires com.pi4j;
requires com.pi4j.library.linuxfs;
requires jsch;
+ requires com.sun.jna;
exports com.pi4j.plugin.linuxfs;
exports com.pi4j.plugin.linuxfs.provider.gpio.digital;
diff --git a/pom.xml b/pom.xml
index 3e8717da..3cad0495 100644
--- a/pom.xml
+++ b/pom.xml
@@ -262,6 +262,7 @@
20020829
1.10.8
0.1.55
+ 5.15.0
5.10.2
2.0.12
2.10.4