http://qs321.pair.com?node_id=11114782

anita2R has asked for the wisdom of the Perl Monks concerning the following question:

Hello,

I am looking for advice on an approach that I can use to solve this problem:

I have a Perl script that runs an lcd display over i2c. This runs continuously, reading and displaying new data from a text file. This runs fine and is stable over many months.

I have a second perl script that reads volts & amps from a device (INA219) that is also connected over i2c. This runs fine when run on a different machine (both Raspberry Pi's).

But ... when both scripts are run on the same machine, the lcd screen gets garbled and some of the amps/volts data is wrong. It seems that the underlying i2c driver does not handle contention. Both scripts use HiPi::Device::I2C.

I have overcome the problem by using time slots.

The amps/volts device can read/write at 0, 5, 10, 15 seconds etc. after each minute and the lcd script can write at (1,2,3) (6,7,8) (11,12,13) seconds etc. after each minute. The data from the amps/volts device is fine and the lcd display is stable using this scheme.

But this is an ugly fix, so I am looking for alternatives.

I looked at using a common module with the aim of passing read/write requests from each script to the module, which would then handle them sequentially, so no contention.

Using two small test scripts and a module I wrote (i2cAccess.pm) I found, perhaps not unexpectedly, that each script loads or uses a separate copy of the module. so there is no common access to the i2c bus. I can post the two scripts and the module if it would be of any use.

I considered having a lock file that each script accesses, to control access to the i2c bus, but that seems like a lot of overhead, probably worse that my time slot solution.

I also setup a test using IPC::Shareable

The aim being to pass the i2c objects, register information etc. to the 'server' program. This failed because, as far as I can tell, Shareable does not support objects. I found a reference to this at https://www.nntp.perl.org/group/perl.poe/2009/12/msg4706.html which says 'Storable can't serialize filehandles'. When adding an i2c object to a shared hash I get 'Can't store GLOB items at ... /IPC/Shareable.pm. Again I can post the test scripts I used if that would be helpful.

Can the monks suggest an approach to handling this issue.

Thank you for your consideration.

Replies are listed 'Best First'.
Re: i2c bus contention
by GrandFather (Saint) on Mar 30, 2020 at 01:16 UTC

    Possibly the solution, if practical, is to combine the scripts. You can then coordinate access to the I2C port. If that is not practical you need to push that management function into a daemon and have your scripts access the daemon to get work done. Without a single "manager" that can serialize access to the I2C buss you are in a world of hurt - as you have already discovered.

    Note that often you will need to manage atomic transactions consisting of a series of writes and reads to get work done. So a manager that just provides atomic read or write of blocks of data probably won't cut it. That may mean the manager needs more domain knowledge than is appropriate for a generic black box or has a complicated API.

    That said, if you combine your common module idea with a lock file you can probably solve the problem without needing to go the daemon route.

    Optimising for fewest key strokes only makes sense transmitting to Pluto or beyond

      If I remember I2C correctly, that is actually over-complicated. The master selects a device by emitting an address onto the bus, possibly writes some number of bytes to that device (a device-internal register number and/or data to write), and possibly reads some number of bytes from that device. The bus then returns to idle state.

      So the manager only needs a slave-address/write-buffer/read-count 3-tuple to define a transaction that can be performed atomically. If read-count is zero, only a completion can be returned; otherwise, the data read from the device is returned. In pseudo-C with the uint8_t[] type as a Perl-like scalar that carries its own length, an I2C transaction has this prototype:

      uint8_t[] i2c_xact(i2c_address slave, uint8_t[] write, size_t read_count);

        In principle yes. In practice it ain't quite as simple as you suggest. The complication isn't the function prototype (or even the implementation) anyway, but even there your suggested API is a little simplistic for practical use. The devil is in the error handling. Not only may either the write or the read fail, but either may partially succeed. So, sticking with pseudo-C/C++ where Buffer is a suitable C++ class equivalent to a Perl string, the function prototype changes a little to:

        Status TransactI2C(uint8_t address, Buffer wData, uint8_t &written, Bu +ffer &rBuffer, uint8_t expected);

        and the calling code needs to be able to handle statuses like: I2C busy, Write failed, Partial Write, Read Failed, Partial Read, Complete OK.

        But the real issue is exactly what the OP is struggling with: simultaneous access. And that isn't solved by just defining a nice API. That has to be solved using some common "process" that serialises access to the I2C port as provided by the OS. That can be non-trivial indeed depending on what the system provides and what your actual use case is. As it happens I've been working to solve exactly those sorts of problems with an embedded system that uses 6 I2C buses, a couple of SPI buses and the odd ADC, USB controller and Ethernet controller thrown in. With all of that stuff running interrupt driven and half of it driving DMA things get pretty interesting!

        Optimising for fewest key strokes only makes sense transmitting to Pluto or beyond

        @jcb As GrandFather notes, I still need to get a common function that handles calls to the i2c bus from two or more scripts.

        My access to the i2c bus from the two existing scripts works well and is stable when contention is managed on a time slot basis.

      @GrandFather I would rather not combine my scripts, as they each have very different purposes, and the two scripts are also used independently on other machines. I think that creating a daemon is worth a try. As I have no formal IT training, this is going to be an interesting learning experience.

      As to the common module with a lock file, I am still stuck with how to have two scripts accessing a common module. I have now tried running a script that has package functions and calls the two test scripts. Pretty messy to put it mildly, but is was just an experiment.

      #!/usr/bin/perl package Hvac::i2cAccess; use strict; use HiPi::Device::I2C; use Exporter qw( import ); our @EXPORT_OK = qw( i2c_test1 i2c_test2 ); my $copyit = "Empty"; # call test scripts # test1 just gets the $copyit value # test2 sends an i2c object and gets the voltage value in return # the voltage value is also copied into $copyit do "/home/huw/cron_scripts/test1.pl"; print "A\n"; do "/home/huw/cron_scripts/test2.pl"; print "B\n"; do "/home/huw/cron_scripts/test1.pl"; print "C\n"; while( 1 ){ sleep 1; } exit 0; # ********************************************************* # sub i2c_test2 { # i2c object passed from test2.pl my $obj = $_[0]; # read 2 bytes of data from bus voltage register (address 0x02) my @bvr; eval{ @bvr = $obj->bus_read( 0x02, 2 ); 1; } or do { $bvr[0] = 0; $bvr[1] = 0; }; # result in upper 13 bits - big-endian order my $busV = pack 'C2', $bvr[0], $bvr[1]; $busV = ( unpack 'S>', $busV ) >> 3; # scale bus voltage register value my $bv = 16; my $bvScale = 4000; $busV = $bv / $bvScale * $busV; $copyit = $busV; return $copyit; } # ********************************************************* # sub i2c_test1 { print "Test 1 subroutine\n"; return "Copy $copyit"; } # ********************************************************* # # must end modules with a 'true' value 1;

      but I don't get past the first 'do'. I presume that 'do' is waiting for the script it calls to end and return a value. 'A' is never printed

      The module code without the 'calls' to the two test scripts was used to test the common module approach, but as I said, it looks like each test script invokes a separate copy of the module.

      If you do wind up needing to use a daemon, TheSchwartz might do it. I recently ran across STEVED's article series that walks through the process step-by-step. I haven't used it myself, but it's first on my list to try next time I need a job queue.

        @cxw Thanks for those links.

        It looks like I have plenty to read. If I do go down that route and it works, I will post a note about it

Re: i2c bus contention
by haukex (Archbishop) on Mar 30, 2020 at 07:44 UTC
    I considered having a lock file that each script accesses, to control access to the i2c bus, but that seems like a lot of overhead, probably worse that my time slot solution.

    Taking a brief look at the code, it appears that what HiPi::Device::I2C is doing is an ioctl on the /dev/i2c* device file, and my guess would be that the Linux kernel doesn't guarantee those to be atomic when there are multiple writers. But I also don't see your issue with a lock file, since I2C writes should be pretty quick - why not just flock the object's filehandle? I don't have an RPi with an I2C device handy to test, but I'd suggest just trying something like this:

    use Fcntl qw/:flock/; ... my $i2c = HiPi::Device::I2C->new(...); ... flock($i2c->fh, LOCK_EX) or die "flock: $!"; $i2c->bus_write(...); flock($i2c->fh, LOCK_UN) or die "flock: $!";

      Well adding locks to the i2c object as suggested by @haukex works fine.

      The volts/amps data looks fine and there has been no disruption to the lcd dislay, albeit for 24 hours or so. As a test I removed the locking on both scripts, and to my surprise there were virtually no faults. I saw one display line corruption, but the display did not freeze or become unreadable as it had done before, but there were intermittent data errors from the amps/voltage devices.

      Perhaps a kernel update has substantially corrected the problem. I started using the time slot code in November 2018 and I am currently running Raspbian Stretch with 4.19.66+ on a Raspberry Pi Model B Plus Rev 1.2, so there will have been updates since November 2018.

      After updating scripts on a second Raspberry Pi (Raspberry Pi 3 Model B Rev 1.2) also running Stretch, there has been a marked increase in stability. One script that previously had unexplained crashes has been stable for almost 24 hours and data errors have stopped. This Pi has three scripts accessing i2c devices on a regular basis.

      When using flock to lock the filehandles, note that the module to use is

      HiPi::Device::I2C;

      The BCM2835 module

      HiPi::BCM2835::I2C qw( :all )

      does not work - you get this message

      Can't locate object method "fh" via package "HiPi::BCM2835::I2C"

      The other gotcha was that using the default i2c bus on pins 3 & 5 (gpio2/gpio3) resulted in this error

      i2c_write failed with return value -1

      This is resolved by opening an alternate i2c bus. Edit the /boot/config.txt file and add this:

      # activate an additional i2c interface on gpio 6(pin 31) & gpio 12(pin + 32) dtoverlay=i2c-gpio,bus=3,i2c_gpio_sda=6,i2c_gpio_scl=12

      You need to be root to edit the file (use sudo). Reboot to activate. You can use any unused pair of gpios, preferably avoiding any that are designated for other functions. The two gpio's used each require a pull-up resistor to 5 volts.

      The additional i2c bus is likely i2c-3. Enter ls -l /dev/i2c* to see the device id's.

      Thanks to everyone for your replies and of course to haukex for a successful conclusion.

      @haukex Thanks for the suggestion. I will try adding the lock

      It will take some time to test, as I will have to deactivate the time slot code in both scripts.

Re: i2c bus contention
by stevieb (Canon) on Mar 30, 2020 at 00:06 UTC

    Need to see all of your code. I am extremely familiar with Perl on the Raspberry Pi, so I may be able to help.

      It seems you're suffering from trying to have a multi-master I2C bus with the two scripts running, but I don't know how that distribution handles that situation.

      @stevieb The code for the lcd module runs to 75K, so you probably don't want to see all of it! Most of it is not relevent to i2c bus access