From 58a6b366a99bca386df8e4462c0ccb99ebf39447 Mon Sep 17 00:00:00 2001 From: Mike Pilone Date: Fri, 4 Oct 2024 20:44:32 -0400 Subject: [PATCH 1/2] Add support for SPI on Pi5 using /dev/spidev. The read methods are untested. JNA is introduced as a dependency. --- plugins/pi4j-plugin-linuxfs/pom.xml | 5 + .../pi4j/plugin/linuxfs/LinuxFsPlugin.java | 12 +- .../plugin/linuxfs/internal/LinuxLibC.java | 59 ++++ .../linuxfs/provider/spi/LinuxFsSpi.java | 281 ++++++++++++++++++ .../provider/spi/LinuxFsSpiProvider.java | 28 ++ .../provider/spi/LinuxFsSpiProviderImpl.java | 48 +++ .../src/main/java/module-info.java | 1 + pom.xml | 1 + 8 files changed, 430 insertions(+), 5 deletions(-) create mode 100644 plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/internal/LinuxLibC.java create mode 100644 plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/provider/spi/LinuxFsSpi.java create mode 100644 plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/provider/spi/LinuxFsSpiProvider.java create mode 100644 plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/provider/spi/LinuxFsSpiProviderImpl.java diff --git a/plugins/pi4j-plugin-linuxfs/pom.xml b/plugins/pi4j-plugin-linuxfs/pom.xml index 3ed501bf..3c0b3d35 100644 --- a/plugins/pi4j-plugin-linuxfs/pom.xml +++ b/plugins/pi4j-plugin-linuxfs/pom.xml @@ -21,6 +21,11 @@ jsch ${jsch.version} + + net.java.dev.jna + jna + ${jna.version} + 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..d1723b4a --- /dev/null +++ b/plugins/pi4j-plugin-linuxfs/src/main/java/com/pi4j/plugin/linuxfs/provider/spi/LinuxFsSpi.java @@ -0,0 +1,281 @@ +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/spidev0."; + private final LinuxLibC libc = LinuxLibC.INSTANCE; + private int fd; + + public LinuxFsSpi(LinuxFsSpiProviderImpl provider, SpiConfig config) { + super(provider, config); + } + + @Override + public void open() { + super.open(); + + String spiDev = SPI_DEVICE_BASE + config().bus().getBus(); + 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 3391540b..57e4b83a 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 From 7ec21725b9dcfb6fc580387093bfcba110be91be Mon Sep 17 00:00:00 2001 From: Mike Pilone Date: Mon, 25 Nov 2024 09:31:48 -0500 Subject: [PATCH 2/2] Updated the device name to properly use the bus and chip select config values. --- .../pi4j/plugin/linuxfs/provider/spi/LinuxFsSpi.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 index d1723b4a..0d4a5223 100644 --- 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 @@ -102,7 +102,7 @@ public static class ByValue extends spi_ioc_transfer implements Structure.ByValu 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/spidev0."; + private final static String SPI_DEVICE_BASE = "/dev/spidev"; private final LinuxLibC libc = LinuxLibC.INSTANCE; private int fd; @@ -114,7 +114,13 @@ public LinuxFsSpi(LinuxFsSpiProviderImpl provider, SpiConfig config) { public void open() { super.open(); - String spiDev = SPI_DEVICE_BASE + config().bus().getBus(); + // 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);