Skip to content

FFM API code base#458

Merged
eitch merged 113 commits intodevelopfrom
ffm-api
Sep 24, 2025
Merged

FFM API code base#458
eitch merged 113 commits intodevelopfrom
ffm-api

Conversation

@DigitalSmile
Copy link
Copy Markdown
Contributor

@DigitalSmile DigitalSmile commented Mar 29, 2025

This PR is work in progress, will be adding new implementations step by step.

  • Overall design
  • Digital Input
  • Digital Output
  • i2c via smbus
  • i2c via ioctl
  • i2c via file write/read
  • spi
  • hardware pwm
  • uart

Implementation details

FFM API provides new robust access to native code from JVM world. Moreover, all pi4j plugins relies on third party native libraries, which in the end calls linux kernel functions.
So, the typical flow was:
Java method (pi4j) -> JNA Java Code (jna) -> JNI Wrapper Call (pi4j) -> Native Library Call (gpiod, pigpio, etc) -> Kernel syscall
Now with the power of FFM API:
Java method (pi4j) -> Kernel syscall
This changes leads to better maintanability, less unsecure native calls, speed and better understanding of the code.

Main package

New plugin consists of several packages:

  1. common package with internal structures needed to FFM work
    • file - all needed code to access file descriptors with open/read/write/close/... methods
    • gpio - all needed structures to work with GPIO (DigitalInput/Output) interface
    • i2c - all needed structures to make I2C work (direct/ioctl/file). The only runtime dependency to work is i2c-tools
    • ioctl - syscall wrapper to work with linux kernel HAL
    • poll - syscall wrapper to work with poll/epoll messages
    • serial - all needed structures to work with Serial interface
    • spi - all needed structures to work with SPI bus
  2. providers package with all user-facing implementations
    • gpio - contains DigitalInputFFM and DigitalOutputFFM implementations
    • i2c - contains I2CSMBus (using i2c-tools userspace library), I2CFile (using simple files) and I2CDirect (using ioctl)
    • pwm - contains PwmFFMHardware implementation
    • serial - contains SerialFFM implementation (beta version for now)
    • spi - contains SpiFFM implementation

Tests

Since testing hardware is non-trivial tasks, I used an approach with different tests for different purposes.
Tests are organized in several packages:

  1. unit - classic unit testing with support of Mockito framework. Mocks are located at package mocks
  2. model - tests, that covers model conversion from Java objects to FFM API structures
  3. integration - tests that are using mock drivers on linux kernel side to simulate the behaviour of real hardware.
    • uses i2c-stub, gpio-sim and custom spi-mock kernel drivers (source code located in native folder)
    • the main sequence: setup drivers -> ensure all drivers are active -> make echo calls with pi4j -> test the return result
    • disabled for now, need to be moved to pi4j-harness project
  4. jmh - classic JMH tests to measure speed of end-to-end calls with FFM/non-FFM plugins.

JMH Test results

Pi4j has 3 providers that are used in common:

  • gpiod provider with access to GPIO pins, uses libgpiod under the hood
  • pigpio provider with GPIO, I2C, PWM, Serial and SPI, uses libpigpio under the hood with access through /dev/mem
  • linuxfs provider with GPIO, I2C, PWM, Serial and SP, uses sysfs under the hood

All tests were run in VirtualBox environment using mock linux drivers:

  • GPIO using sim-gpio driver
  • I2C using i2c-stub driver
  • SPI using custom self made echo driver
    Setup scripts are located here.

Because we are using mock drivers, we cannot test pigpio as it requires real hardware with available DMA addresses, so this library is out of scope of performance benchmark.
PWM is excluded from performance tests, because it is relying on filesystem in every provider available.
Serial is excluded, because FFM implementation is in beta and is out of scope with this PR.

How to read results

The results should be analysed in terms of relative numbers, but not absolute. The less is average time - the best score it wins.

GPIO (tested September, 20 2025)

  • OpenJDK Runtime Environment Corretto-25.0.0.36.2 (build 25+36-LTS)
  • Linux ds-VirtualBox 6.14.0-29-generic #29~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Aug 14 16:52:50 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
  • AMD Ryzen 7 5800X 8-Core Processor 29GiB System memory
    Results:
Benchmark                                                  Mode  Cnt     Score    Error  Units
GPIOPerformanceTest.testFFMInputRoundTrip                  avgt    5     0.173 ±  0.003  ms/op
GPIOPerformanceTest.testGpioDInputRoundTrip                avgt    5    10.537 ±  6.196  ms/op
GPIOPerformanceTest.testLinuxFsInputRoundTrip              avgt    5   111.781 ± 42.199  ms/op

Benchmark                                                  Mode  Cnt     Score    Error  Units
GPIOPerformanceTest.testFFMInputWithListenerRoundTrip      avgt    5     1.594 ±  5.830  ms/op
GPIOPerformanceTest.testGpioDInputWithListenerRoundTrip    avgt    5     9.015 ± 12.139  ms/op
GPIOPerformanceTest.testLinuxFsInputWithListenerRoundTrip  avgt    5   110.115 ± 18.796  ms/op

Benchmark                                                  Mode  Cnt     Score    Error  Units
GPIOPerformanceTest.testFFMOutputRoundTrip                 avgt    5     0.042 ±  0.001  ms/op
GPIOPerformanceTest.testGpioDOutputRoundTrip               avgt    5     0.110 ±  0.002  ms/op
GPIOPerformanceTest.testLinuxFsOutputRoundTrip             avgt    5   108.306 ± 12.490  ms/op

SPI (tested September, 20 2025)

  • OpenJDK Runtime Environment Corretto-25.0.0.36.2 (build 25+36-LTS)
  • Linux ds-VirtualBox 6.14.0-29-generic #29~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Aug 14 16:52:50 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
  • AMD Ryzen 7 5800X 8-Core Processor 29GiB System memory
    Results:
Benchmark                                         Mode  Cnt  Score   Error  Units
SPIPerformanceTest.testFFMWriteReadRoundTrip      avgt    5  0.044 ± 0.006  ms/op
SPIPerformanceTest.testLinuxFsWriteReadRoundTrip  avgt    5  0.076 ± 0.001  ms/op

I2C (tested September, 20 2025)

  • OpenJDK Runtime Environment Corretto-25.0.0.36.2 (build 25+36-LTS)
  • Linux ds-VirtualBox 6.14.0-29-generic #29~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Aug 14 16:52:50 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
  • AMD Ryzen 7 5800X 8-Core Processor 29GiB System memory
    Results:
Benchmark                                 Mode  Cnt  Score   Error  Units
I2CPerformanceTest.testFFMSMBusRoundTrip  avgt    5  0.038 ± 0.002  ms/op

@DigitalSmile
Copy link
Copy Markdown
Contributor Author

For the history, benchmark for the simple roundtrip (create, get state, shutdown):

Result "com.pi4j.plugin.jmh.DigitalInputPerformanceTest.testCreateShutdownRoundTrip":
  1.652 ±(99.9%) 0.121 ms/op [Average]
  (min, avg, max) = (1.615, 1.652, 1.698), stdev = 0.031
  CI (99.9%): [1.531, 1.772] (assumes normal distribution)

@eitch
Copy link
Copy Markdown
Member

eitch commented Mar 31, 2025

For the history, benchmark for the simple roundtrip (create, get state, shutdown):

Result "com.pi4j.plugin.jmh.DigitalInputPerformanceTest.testCreateShutdownRoundTrip":
  1.652 ±(99.9%) 0.121 ms/op [Average]
  (min, avg, max) = (1.615, 1.652, 1.698), stdev = 0.031
  CI (99.9%): [1.531, 1.772] (assumes normal distribution)

Perhaps you can add a benchmark for the existing provider?

@DigitalSmile
Copy link
Copy Markdown
Contributor Author

For the history, benchmark for the simple roundtrip (create, get state, shutdown):

Result "com.pi4j.plugin.jmh.DigitalInputPerformanceTest.testCreateShutdownRoundTrip":
  1.652 ±(99.9%) 0.121 ms/op [Average]
  (min, avg, max) = (1.615, 1.652, 1.698), stdev = 0.031
  CI (99.9%): [1.531, 1.772] (assumes normal distribution)

Perhaps you can add a benchmark for the existing provider?

That's for the the stage 2 :) I tried to quickly do it and failed, due to lack of build targets for desktop (amd64) of natives. I can try to run the benchmark on a real hardware, but since we are testing in comparison with other approaches, it is better to stay with gpio-sim.
Anyway will try to build locally later on.

@DigitalSmile
Copy link
Copy Markdown
Contributor Author

Okay, a bit of journey on benchmarks :)

TLDR

For libgpiod (Java code -> JNA Code -> libpi4j-gpiod.so (JNI-wrapper) -> libgpiod.so (native library) -> GPIO kernel syscalls)

Result "com.pi4j.plugin.gpiod.jmh.DigitalInputPerformanceTest.testCreateShutdownRoundTrip":
  0.252 ±(99.9%) 0.015 ms/op [Average]
  (min, avg, max) = (0.248, 0.252, 0.257), stdev = 0.004
  CI (99.9%): [0.236, 0.267] (assumes normal distribution)

For FFM API (Java code -> GPIO kernel syscalls)

Result "com.pi4j.plugin.ffm.jmh.DigitalInputPerformanceTest.testCreateShutdownRoundTrip":
  0.167 ±(99.9%) 0.008 ms/op [Average]
  (min, avg, max) = (0.165, 0.167, 0.170), stdev = 0.002
  CI (99.9%): [0.159, 0.175] (assumes normal distribution)

Bit of details

So first things first I built manually all gpiod natives (that was a lot more easier without maven) and pointed to the library folder in the code of NativeLibraryLoader class. Then I had to comment out lines in GpioDContext to prevent failing since I am not running Pi (btw, why there is such a limitation?).
My naive approach was to write the similar test like in https://github.com/Pi4J/pi4j/blob/e1f3079e19c0025271cb7bb947b6e05023e86038/plugins/pi4j-plugin-ffm/src/test/java/com/pi4j/plugin/jmh/DigitalInputPerformanceTest.java The problems begun immediately with SIGSEV from JVM, indicating that memory is corrupted.
malloc(): unsorted double linked list corrupted
I digged and found out, that threads created by monitorLineEvents are usually freeze and unable to shutdown properly. It seems the heap goes crazy in between native calls <-> monitoring thread <-> main java code. When I commented out the monitoring submission to ExecutorService all benchmarks went fine giving me above result.

So for the sake of comparison I did the same thing with FFM code (commented out monitoring thread creation) and rerun benchmark.

The results are as expected almost similar, with a slight faster with FFM. That happens, just because the roundtrip between native world and Java world is shorter.
Anyway, all above is jfyi, so I don't think we need to take any action items on that. I will continue with feature implementations. Next is i2c.

@eitch
Copy link
Copy Markdown
Member

eitch commented Apr 2, 2025

Nice write up! Thanks a lot for that insight!

// if 'flags' were provided in the SPI config, then accept them
if(config().flags() != null){
flags = config().flags().intValue();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the linuxfs spi implementation we error out is the flags parm was used, This pertained to the pigpio provider and it continued use would put limits on the linuxfs code.

if(bus.getBus() > 1){
throw new IOException("Unsupported BUS by PiGPIO SPI Provider: bus=" + bus.toString());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, ignoring the old pigpio limitations, the linuxfs spi will allow any bus and channel the user wants. They are required to do the config.txt updates to the spi actually exists.

if(bus == SpiBus.BUS_1 && (mode == SpiMode.MODE_1 || mode == SpiMode.MODE_3)) {
throw new IOException("Unsupported SPI mode on AUX SPI BUS_1: mode=" + mode.toString());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

broken record, this limitation does not exist in linuxfs

@DigitalSmile
Copy link
Copy Markdown
Contributor Author

@taartspi just for the record - SPI classes are just the boilerplate code now, I copy-pasted it for future reference. You can have a look on my implementation here (which I'll be using): https://github.com/DigitalSmile/gpio/blob/main/src/main/java/org/digitalsmile/gpio/spi/SPIBus.java

@taartspi
Copy link
Copy Markdown
Contributor

Looks the merge is complete and ffm updates are ready for HW testing

Copy link
Copy Markdown
Contributor

@taartspi taartspi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reviewed parts of the code not all. This delivery is a great deal of work. I will continue to review and test I think we should merge and build this to make testing and any new issue discussions easier.

@eitch
Copy link
Copy Markdown
Member

eitch commented Sep 24, 2025

So i'll update develop to 4.0.0-SNAPSHOT and then merge this branch.

Any objections? @FDelporte @DigitalSmile @taartspi

@FDelporte
Copy link
Copy Markdown
Member

@eitch the branch is already on <version>4.0.0-SNAPSHOT</version>.
So yes, please, let's merge and speed up testing 🚀
There is only one direction: FORWARD 😉

@eitch eitch merged commit b18532e into develop Sep 24, 2025
1 check passed
@eitch eitch deleted the ffm-api branch September 24, 2025 07:22
@eitch
Copy link
Copy Markdown
Member

eitch commented Sep 24, 2025

Done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants