[ANN] Introducing BaremetalPi.jl - A package to access Raspberry Pi peripherals without external libraries

Hi!

As I posted here: Have a try Julia v1.4.2 for arm32bit, I was programming a package for Julia to access the Raspberry Pi peripherals without requiring external libraries.

Currently, we have some nice packages to use the Pi such as PiGPIO.jl. However, they did not fit my use case. My ultimate goal is to use Julia to control the attitude of a CubeSat (a very, very small satellite). I will face some problems due to the nature of Julia, and I need to keep things as standalone as possible. PiGPIO.jl uses the library pigpio, which runs a daemon that opens a socket that you can access to control the peripherals. Those things (socket, daemon, etc.) will introduce too much trouble for my problem. Hence, I decided to create a package that access everything without any external libraries. This led to BaremetalPi.jl. I have already asked for registration and should be available in the Registry in 3 days :slight_smile:

This package has the following features:

  • Full control of GPIO (set mode, change level, read level, PWM is not available yet): everything is done by mapping /dev/gpiomem.
  • SPI (full duplex transfers): we can manage multiple SPI devices with full duplex transfers. Everything is done by issuing ioctl using /dev/spidevX.Y devices.
  • I2C (SMBUS): there are many commands to manage I2C devices, such as read/write byte, word, and blocks. Everything is done by issuing ioctl using /dev/i2c-X devices.

Ah, the good side is that the user does not need to be root, they only need permission to access the required devices.

Documentation

An initial version of the documentation can be seen here. I also added two simple examples to clarify the usage of the package. One is the hello world (make a led blink) and another is a more complicated example that reads the ambient temperature using the AD converter MCP3008 through SPI interface.

Latency

My ultimate goal needs a more or less strict real-time performance. I let the Raspberry Pi acquiring the temperature every 5 min during almost 4 days and I got this latency:

Notice that I neither used a kernel with PREEMPT_RT nor disable the Julia garbage collector. I also did not tune the system by stopping unnecessary processes. Hence, since the deadline was only real lost in 2 occasions (for my needs), it really seems promising :slight_smile:

Development

Unfortunately, I could only test this package in the RaspberryPi Zero W. Furthermore, only the capabilities of I2C and SPI supported by my devices could be verified. Hence, I would love if people with other models could test and help me improve this package :slight_smile:

38 Likes

How are you measuring “latency”? I’m wondering why there would be negative values.

2 Likes

The process is scheduled to be executed every 300s. Then I just subtract the values obtained by the command time(). What I plotted is the difference between the obtained value and 300.

EDIT: Now I realize that maybe the name in English is not latency, but I think you know what I tried to measured :slight_smile:

1 Like

I didn’t mean to be pedantic. :wink: I was curious how much of the variation was an artifact of the timing measurement itself, especially because of the apparent negative autocorrelation.

1 Like

This looks great!

My 3B+ is in a remote location in my house so I’m not putting a scope on it tonight, but your library installed (on Julia 1.2.0) and I was going to say that the pkg> test BaremetalPi passed but it seemed kinda quick and then I looked at runtests.jl … and maybe I should contribute some tests! I was thinking about some loopback sorts of things but since you don’t know what’s hooked up to any particular board, how do you safely do that?

Sometime in the next few days I’ll put the Pi on my desk and put the scope on GPIO pins and the serial buses. I’ve been wanting to use Julia for rapid prototyping and this is a real step in that direction!

1 Like
julia> function kflips()
         for i in 1:1_000_000
           gpio_set(2)
           gpio_clear(2)
         end
       end
kflips (generic function with 1 method)

julia> @time kflips()
  0.102462 seconds (24.23 k allocations: 1.003 MiB)

julia> @time kflips()
  0.036992 seconds (4 allocations: 160 bytes)

julia> @time kflips()
  0.036015 seconds (4 allocations: 160 bytes)

julia> @time kflips()
  0.036435 seconds (4 allocations: 160 bytes)

julia>

If I’m not getting fooled here, we’re averaging 27 MHz on a bit-banged GPIO pin! If I try to read the pin inside the loop immediately after writing it and @assert its value, I get failures, but that may well be correct behavior due to execution pipelining and input synchronization (in other words, you can issue a write to a gpio followed by a read, and get back the previous value of the gpio pin).

I’ll put a scope on it soon.

4 Likes

I can help to test on a Pi 3B

1 Like

Good! Sorry about the tests :slight_smile: I forgot to mention that I did not figure out how can I test the package. The script in runtest.jl is just a dummy that must pass on Travis so that the documentation gets built. Currently, I am testing the functionality using my Raspberry Pi manually, but it would be very, very nice if we design some kind of test set. It just need to stay out from runtest.jl, I think.

Yeah, I think it needs sometime between writing into the register and be able to read the value. The following script sometimes fail, sometimes not:

julia> using BaremetalPi

julia> init_gpio()

julia> gpio_set_mode(4,:out)

julia> for i = 1:1000
       gpio_clear(4)
       @assert gpio_read(4) == false "ERROR"
       gpio_set(4)
       @assert gpio_read(4) == true "ERROR"
       end

However, if we add a yield() between set and read, then it always passes here:

julia> for i = 1:1000
       gpio_clear(4)
       yield()
       @assert gpio_read(4) == false "ERROR"
       gpio_set(4)
       yield()
       @assert gpio_read(4) == true "ERROR"
       end

Good! If you can provide feedback if things are working using what you have (SPI, I2C, GPIO, etc.), I will really appreciate :slight_smile:

To avoid this issue, in BinaryBuilder.jl, where tests on Travis often fail because of connection issues, we build documentation with GitHub Actions, so that it doesn’t depend on the outcome of Travis tests

2 Likes

Good advice! Thanks, the idea seems very good, I will see how can I implement this :slight_smile:

Update! It seems that the PREEMPT_RT patch helped a little bit, but I still see a deadline miss:

Hence, I need to test two things now: 1) garbage collector and 2) the OS. For the latter, I am building Julia for Archlinux to verify any diferences. For the former, I still need to figure out what should I do. I have two options: 1) avoid allocations at all costs and disable GC, or 2) disable GC and make it run only during certain moments. Any advice?

1 Like

There’s been talk of doing interesting stuff with memory management like escape analysis using new compiler infrastructure coming in 1.6

This would be a good project to help shape and motivate the work that Keno is driving here: GitHub - Keno/Compiler3.jl: Staging package for new compiler interfaces

1 Like

So this is really cool! PiGPIO was the quickest way to using Julia on the Pi, but I’ll be glad if we can stop using it. The latency is high, and its difficult to do callbacks and events (that still doesn’t work very well).

The one thing I use a lot that is missing here is PWM. Will that be very difficult to support? Also we’ve collected a decent set of examples and tutorials in PiGPIO.jl, it would be good to get them ported.

As to GC, I think for low latency applications, switching off GC in the hot loop is a good strategy. You’ll need to preallocate everthing, run your code once to compile all your functions, and then switch of GC before starting your hot loop. However, I think this should be not be in the “framework” but be handled by user code. The framework does not know what the user is doing with its output, so it has to be up to the user to manage their memory. But for this to work, the framework should be written to only allocate on stack. Which is a bit tricky, but certainly possible, since the framework itself is basically reading and writing integers from a device, right?

Regards

Avik

4 Likes

Nice! That’s what I thought. We will have more flexibility with a standalone solution.

I read the documentation and it does not seem to be complicated. The only “problem” is that we do not have a kernel mapping of the memory like GPIO. Hence, to access the PWM, we need to access /dev/mem. This mean that we will need to be root and that Julia will have access to the entire memory. I think I can do something in the following days :slight_smile:

Yes! Thanks for the tip and you are completely right. For GPIO, BaremetalPi.jl does not allocate at all (only at the initialization). For SPI, we have some allocations, but I will modify it by having two types of functions, like:

# Read 2 bytes starting from register 0x07 from selected I2C device.
julia> i2c_smbus_read_i2c_block_data(1, 0x07, 2)
2-element Array{UInt8,1}:
 0x30
 0x01

julia> i2c_smbus_read_i2c_block_data!(buffer, 1, 0x07, 2)

julia> buffer
2-element Array{UInt8,1}:
 0x30
 0x01

EDIT: Can you please tell me what kind of functionalities related to PWM do you use? So I can focus on them initially?

2 Likes

Rather than using the memory mapped interface, it would be nice to use the sysfs PWM interface. This solution will work on boards besides the Pi (I know it works on the BeagleBone and some Allwinner boards, Julia doesn’t work there yet but I have hope!). It also has simpler permission requirements, only the sysfs nodes need permissions so you don’t need to run as root.

A similar interface exists for GPIO, but it is apparently deprecated. I haven’t yet used a new enough kernel to try the new way, so I suspect in practice the sysfs way will be useful for a while to come.

4 Likes

Good! Thanks! I will see if we can use the sysfs interface without any compromises :slight_smile:

Does it make sense to separate out this function into a tiny C program and talk to it over a pipe? More latency perhaps, but PWM cycles operate at tens of kilohertz or something, not tens of megahertz so it’s less important, ms latency would be fine right? And the benefit to security would be MUCH.

EDIT: I didn’t read far enough. yes the sysfs interface would be good too!

1 Like

Yes, I am reading about it. I think we can use the sysfs interface for now :slight_smile:

1 Like

I’m comparing this to use of micropython on pyboards. On those I can do a bunch of work (e.g. a PID loop calculation and updating of a PWM or GPIO states) in a few hundred microseconds to a millisecond. I don’t know how much overhead pipes involve as opposed to using ccall to hit a .so file, buy the latter is fast.

So in GPIOZero, the most popular user friendly library for using GPIO pins in python, they depend on different backends (pin libraries) such as PiGPIO or RPi.GPIO. If it doesn’t find either of those, it falls back to pure python interface, which does pretty much what your’ve done here.

The exception is software PWM, which needs the CPU to signal at the PWM frequency. This will typically need a native thread. Hence, GPIOZero does not contain a pure python software PWM implementation, it always tries to use the pin libraries. If it cant find a backend, then the pure python implementation will not support software PWM.

Here is the C implementation of software PWM in Rpi.GPIO: raspberry-gpio-python / Code / [be8e4d] /source/soft_pwm.c [MIT license] . There is another C implementation in PiGPIO.

Julia does have native threads now, but using them performantly in this fashion may not be trivial. It’s worth trying, but using a small C library for this might be easier?

Hardware PWM can be supported without threads, but that is available on only one pin on the pi, afaik.

[Thanks to Ben, the author of GPIOZero for helping me figure this out]

Can you please tell me what kind of functionalities related to PWM do you use? So I can focus on them initially?

Apart from the trivial thing of changing an LED brightness, its useful to to use PWM to control the speed of a motor using a motor controler (such as the one on the explorer hat) https://github.com/JuliaBerry/JuliaBerry.jl/blob/master/src/JuliaBerry.jl#L96

1 Like