Sysfs vs. mmap for GPIO/PWM/DMA control

#1

Hi, I’m flirting with the idea of creating a pure rust port of the rasperry pi ws2811 library with an end goal of creating a PoC for a Falcon Player replacement in Rust (lots of people in the community complain of crashes, perhaps a Rust version could improve the situation).

I’m a java guy by day, and also new to low level IO programming, so I’ve been using google a lot to try and understand what it might take to achieve this project. Most of my searches are landing on code/posts from @japaric and I’m generally getting the idea that I should be looking into basing my project upon the work on the embedded-hal.

And now to my main question. The code in rpi_ws281x uses mmap to access/control the gpio, pwm and dma, whereas the embedded-rust community have established libraries for managing gpio and pwm via the sysfs interface. Are there any potential consequences of using the sysfs interface instead of implementing an mmap version of the same functionality? Potential perfomance impact, for example?

I don’t see anything similar for the DMA access, yet sysfs appears to have an interface for that too, should I continue down that route?

My general plan for implementing this would be;

  1. clone rpi_ws281x and rip out all code relating to SPI, PCM, and reduce it to only one PWM output (this is to reduce my scope as small as possible, while remaining useful and help me to understand the existing code).
  2. write a small demo app to ensure I didn’t break the above code
  3. Start porting the code to rust implementing/using the functionality from embedded-hal/sysfs-gpio/sysfs-pwm
  4. Once working parameterize to add back in the functionality I scoped out in 1.

Does this sound like a sane way forward?

Note: I’m aware of the rust wrapper for ws281x (not linked as I’m only allowed two links as a new user) but I thought that a pure rust port might also be an interesting exercise.

#2

I just stumbled across the register-rs crate, using this I think it would be trivial to port over the existing c library directly as it contains clear descriptions of all the register locations… starting to think that would be a good first step and save the hal APIs until a later refactoring.

#3

If safety is a concern, then you should stick to the SPI interface, using the spidev interface. This is one of the methods that the rpi_ws281x implementation uses, so you can look at it for inspiration. This uses a device exported from the Linux kernel that is designed to support userspace access directly to the SPI hardware, and the DMA aspect is abstracted away for you via the standard read and write interfaces.

The other implementations in rpi_ws281x, using PWM and PCM hardware directly by mmaping from /dev/mem, and using the videocore coprocessor to set up DMA, do so by directly poking at memory, bypassing the kernel in ways that are unsafe. Take a look at the caveat from the README, where it mentions that picking the wrong parameters can conflict with the kernel use of the same hardware:

You must make sure that the DMA channel you choose to use for the LEDs is not already in use by the operating system.

For example, using DMA channel 5 will cause filesystem corruption on the Raspberry Pi 3 Model B.

The default DMA channel (10) should be safe for the Raspberry Pi 3 Model B, but this may change in future software releases.

As you can see, an updated kernel or a new driver or something that uses a driver that this author didn’t anticipate could cause a conflict and undefined behavior.

These methods are basically poking at hardware that is supposed to be owned and arbitrated by the Linux kernel, but the kernel doesn’t provide a userspace interface that is sufficient for the purposes of implementing the ws281x protocols; for instance, the kenel provides a generic PWM interface, but you can just set some basic parameters on frequency and duty cycle, you can’t vary the PWM based on DMA data like you need to to implement the ws281x protocol.

To safely do this on Linux, you’d either need to export a more comprehensive kernel driver that exposes more of the hardware functionality from the kernel, or write your driver itself as a kernel driver.

The spidev interface is probably your best bet if you want to do something which is safe, and less likely to run into memory errors. You could use it directly, but you can also use the spidev crate which already provides and interface to it; and which is used by the linux-embedded-hal crate to provide an embedded-hal compatible implementation of the embedded_hal::spi::FullDuplex trait.

So you could use spidev crate directly, but if you want your crate to be more portable to other embedded platforms, you could have the core of your driver just depend on the embedded_hal::spi::FullDuplex crate, and then just use linux_embedded_hal::Spidev to create and initialize an appropriate device on your Raspberry Pi.

#4

Thanks for the detailed response. While improved safety is a stated goal of what I was planning to achieve here, it’s still going to be trumped by functionality. If I understand correctly the RPi only exposes one SPI interface that can be used wheras using PWM the Falcon Player is able to drive two strings of “pixels” on most Pi models, quite often paired with the PiCap that steps up the voltage on the signal (amongst other things). I believe that going over to SPI for creating a safer Falcon Player would greatly reduce the functionality.

That said, I have another project where I want a little more detailed control over the leds, so I could start by implementing the SPI interface as a way to get friendly with the embedded-hal apis before going forward with the less safe PWM implementation.

#5

Note to my future self when I finally get around to this project.

rppal looks like it might be interesting for this project https://github.com/golemparts/rppal