Wanted: A good SPI mutex

I really hate doing that unless the posts are 99% angst and another 10% frustration talking, as I believe the process that gets you to the destination is as important as the destination itself.  There’s usually a lot of irreplaceable experimental knowledge along the way!

And You deleted all the example classes!!!  I wanted to copy them!!!  (Growl, growl, hiss, hiss)

Wasn’t it Einstein who said “I’ve learned much more from my failures than I ever did from my successes.”

:wink:

2 Likes

Oh, and P.S.

Great find!

I’m sorry that I can’t give that posting five or ten likes as it is absolutely the correct solution, (Qualifier: Within the context of the GoPiGo robot, but that’s the requirement anyway so it’s all good!)

Thanks SO MUCH for all your help and effort you’ve put into to this project.  And the education you’ve given me along the way!

2 Likes

You mean the several “doesn’t work” and the one “hard” way to do it on the GoPiGo3? No one should ever look at those. One of the principles of learning is never learn wrong stuff, and another is don’t learn from bad examples.

If you want an example - read the beautiful code the amazing DI folks wrote (on your robot at):
/home/pi/Dexter/lib/Dexter/RFR_Tools/miscellaneous/di_mutex.py

and on Github at: https://github.com/DexterInd/RFR_Tools/blob/master/miscellaneous/di_mutex.py

It is pure Python and Linux, no Dexter dependancies, and will run on any robot using the:

  • put it somewhere in the Python path

or

  • put it somewhere and modify the Python path with:
import os
os.path.insert(1,'/path_to_di_mutex.py/')

2 Likes

I found the following thread:

. . . and there was discussion of making the use of the mutex the default.

Question:
Aside from the “pedagogical benefit” of making an inadvertent fool out of the unsuspecting student, is there any real disadvantage to having the mutex on full-time?

2 Likes

Yes, it increases the risk that the mutex lock file will get locked by a zombie, and require a ghost-buster visit.

2 Likes

Ref:  The following quotes:

. . . and then

Does this mean you do this, but accept the risk that you may end up hung somewhere if the mutex gets deadlocked[1]?

If that’s true, how often do you experience this, if at all?

========== Footnotes ==========

  1. That is, the risk is POSSIBLE though UNLIKELY, as in “going outside” has various risks, (getting hit by lightning[2], an errant airplane landing on your head, getting bitten by a biker-gang of badgers, etc.), though the probability may be quite small.

  2. That actually happened to an uncle of mine many years ago.
    He was out playing golf at a golf course in Norfolk, Va., with a bunch of his buddies on a warm and sunny day, and he was using an iron to get from the fairway to the green.  When he swung the club it got struck by lightning, (from who-knows-where), and it struck him dead on the spot.  Nobody knows for sure, but the suspicion is “heat lightning”.  (i.e.  A “bolt from the blue” as noted below.)

Ref:
https://www.theweathernetwork.com/ca/news/article/lightning-safety-tips

1 Like

Yes I have to use mutex=True, and even one step further for programs which need read access to the GoPiGo3 but will never issue any effector commands, I created a no_init_easygopigo3.py which allows the initialization option (use_mutex=True, no_init=True) to turn off resetting the GoPiGo3 default speed to 300. Most of my programs set the default to 150 DPI, so if a second program that just wants to read the battery resets the speed to 300, the first program’s commands can be changed mid-motion.

Both Carl and Dave set use_mutex=True and roughly once every 4 to 9 months I have to reboot to clear the I2C error 5 of unknown cause.

Don’t know if you recall, but I had to create an I2C mutex protected BNO055 DI EasyIMU class - for some reason the DI IMU did not get the use_mutex=True option when they added the Software I2C with three channel I2C mutex (global, AD1, and AD2 connected I2C).

Dave no longer has a DI IMU (requiring clock stretching), and I have removed his DI Distance Sensor in light of having a 360 degree LIDAR). I have not seen an I2C error 5 on Dave since.

I do not think I have ever seen the mutex zombie locked, but Carl’s health checker does not test for it.

It is “daily thunderstorm season” here, so much of my days are spent counting the speed of sound seconds before venturing out of the house. We have “armour piercing lightning” roll through that did not read the book on Faraday Shields.

2 Likes

prior updated with this after you saw it.

2 Likes

When I was “a wee lad” growing up in the southeast corner of Virginia, there was one steadfast, engraved in depleted Uranium, don’t even THINK of disobeying rule:

  • Thunder stops EVERYTHING and everyone immediately goes indoors.
    Even if it’s not raining, if you hear thunder - that’s it.

On the other hand, a summer shower, sans thunder, was a treat because it was God’s sprinkler we kids used to cool off in the 90°+ heat.

2 Likes

Great idea, except for one tiny fly in the ointment:

The resultant filename would be “DI_Mutex_[name]”

What I want is a generic SPI mutex that will be suitable for anyone’s code - that I could push to Waveshare or Adafruit, or [etc.].

What I did was to make a copy of di_mutex and rename it to spi_mutex, with the following changes:

class SPI_Mutex(object):
    """ Generic SPI mutex """

    def __init__(self, loop_time = 0.0001):
        """ Initialize """

        self.Filename = "/run/lock/SPI_Mutex"
        self.LoopTime = loop_time
        self.Handle = None
  1. I changed the name from DI_Mutex to SPI_Mutex.
  2. I removed the “name” parameter since it is automatically “SPI_Mutex”
  3. I changed the descriptive text.

Hopefully this will work and create a generic SPI_Mutex lock.

Viz,:

from spi_mutex import SPI_Mutex

spi_mutex = SPI_Mutex


try:
    spi_mutex.acquire()
    # do protected SPI access stuff here
finally:
    spi_mutex.release()

Yes, I know that the DI_Mutex class already exists and - for my very specific GoPiGo application I can use it.

However, I am thinking of a generalized mutex that anyone can use without all the references to Dexter Industries.

Attached:
Entire library code of the proposed spi_mutex library.  Please examine.
spi_mutex.py (2.1 KB)

2 Likes

Really? You are trying to solve “world peace” for all the non-Dexter uses? Good luck.

Previously when attempting to solve the generalized SPI mutex problem, you balked at having to put a generalized mutex code somewhere accessible to all SPI users. So now where are you going to put it? In a venv?

Stick with solving your problem and be happy if the DI mutex solves your problem on your robot. Do you really want to invest all the time and effort to making a generic solution which in actuality is one specific solution for a virtual crowd of “users” you don’t know and don’t know you?

2 Likes

Question:
If you “enclose” a bit of class method code within a mutex block as above, does all the code referenced by that code get included?

Assumptions:

  1. I am assuming that nothing is multi-threaded.
    Actually, I don’t know if ANYTHING within the GoPiGo library code is multi-threaded. :man_shrugging:
  2. Everything necessary has already been included.
  3. I am also assuming that the pseudo-code I’ve written “works”.  (Though you can correct obvious usage errors.)

Viz.:

[class instances]
spi = SPI_Communication_Library
spi_mutex - SPI_Mutex
something_else = Some_Other_SPI_Class

[. . . . some code goes here . . . .]

#  Do an SPI communication transaction
    try:
        spi_mutex.acquire()
        spi.comm(data, address, [r|w])
    finally:
        spi_mutex.release

Where within “spi.comm” there is a reference to “something_else.method”

Viz.:

def spi.comm(data, address, direction)
    [some code]

# "Some_Other_SPI_Class is an additional class that's
# essential for SPI communication.
# (maybe it bit-bangs the bytes?)

    something_else.method(parm1, parm2, parm-n)

    [more code]

    return(result)

Do enclosed classes/methods “inherit” the mutex’s protection?

2 Likes

When I questioned Waveshare about mutexes to see if they’ve already done anything like this, (and if they’ve already proved it doesn’t work, why reinvent the wheel?), they requested a pull request to their repo if I got a mutex working.

So, I figured I’d give it a try.

All I’m going to do is provide the mutex library in the same way that Waveshare provides its own libraries.  If the user wants to put this stuff in a venv, that’s the moose’s problem. :wink:

1 Like

Here is a concrete example:

“spi_transfer_array” is the method called by everything else in the class(es) for any SPI communication

    def spi_transfer_array(self, data_out):
        """
        Conduct a SPI transaction

        Keyword arguments:
        data_out -- a list of bytes to send. The length of the list will determine how many bytes are transferred.

        Returns a list of the bytes read.
        """
        result = GPG_SPI.xfer2(data_out)
        return result

However, spi_transfer_array references another class/method, “GPG_SPI.xfer2”, which is defined near the top as:

    import spidev

<snip additional code>

if hardware_connected:
    GPG_SPI = spidev.SpiDev()
    GPG_SPI.open(0, 1)
    GPG_SPI.max_speed_hz = 500000
    GPG_SPI.mode = 0b00
    GPG_SPI.bits_per_word = 8

. . . where GPG_SPI is an instance of spidev, and I am assuming that spidev does the actual bit-banging of the SPI port, etc.

Does spidev, as instantiated here, become a part of the mutex, or do I have to surround the spidev code itself?

2 Likes

Please think about the sequence of “how any SPI code is called?”

If the caller (and all other callers) pays attention to the mutex, the action is mutually exclusive.

2 Likes

So you have to create a setup to create the egg with the mutex code, or you have to fork the entire waveshare library and add your mutex to their codebase and egg setup.

And BTW, setup tools is deprecated and I don’t know what the new library creation tool is. My modified-for-Bookworm GoPiGo3 API still uses setup, and I ignore the deprecated warning.

2 Likes

I am translating this to mean that:

  1. spi_transfer_array calls GPG_SPI, and GPG_SPI (eventually) does the work via spidev and returns.
  2. If I don’t call spi_transfer_array until I have a lock, and release it when I get the result, everything within it will be protected.
1 Like

No idea what that is.

with mutex.acquire()
    protected stuff stuff
    mutex.release()

IIRC…. The usage pattern

They don’t have eggs, just a lot of discrete chickens. (.py files) for their libraries. All I would need to do is push mine to their repo.

In the gopigo library there is a “master” routine that sends SPI messages called spi_transfer_array as noted in the code snippit above.

The active part of it is a direct call to spidev which is instantiated as GPG_SPI.

My thought is to “wrap” the call to GPG_SPI in a mutex “try” block.

I suspect that spidev opens the SPI channel, asserts the correct chip select line, bit-bangs the data, releases the chip select line, and returns whatever value(s) are returned.

If my assumption is correct than “wrapping” the GPG_SPI with the mutex should be sufficient.

This assumes that there are no other paths to spidev, (or hard-coded SPI routines), outside of the ones I’ve found. If there are other, undocumented, SPI paths I’m doomed!

1 Like

Right, wrap all operations involving the GPG_SPI object AND the WaveShare object.

DI uses ifMutexAcquire() and ifMutexRelease() because the Easy Sensors can be instantiated to use the I2C mutex or not, not being the default, because 99% of DI product programs are single threaded. The fact that the waveshare device has a daemon based API rather than a single threaded API means you don’t have that luxury. Also the I2C mutex has to cover three I2C interfaces - HW I2C, SW1, and SW2 I2C.

1 Like