Help Me Drive an E-Ink Display Using Julia!

Hi folks! :wave:

I am on a quest to use Julia to drive an E-Ink Display that I am running from a Raspberry Pi 4 (Raspbian OS 64-bit). Here is an image of what I would like to be seeing happen:

eink_python

This is using a Python library ā€“ but letā€™s use Julia instead! This little post is me summarizing what I am trying to do, what I have accomplished so far, and obstacles I have encountered.

Please help! I would love to put this up on my wall:

Goal: A Julia-Driven E-Ink Display :dart:

My mission is simple: I want to drive an E-Ink display using Julia running on a Raspberry Pi 4! Specifically, here is the hardware and software I am using:

I have wanted to do this for quite some time and now am on holidays ā€“ so, I have some time now to tackle this fun engineering problem! Specifically, what I would like to accomplish in terms of features is the following:

  • Communicate and update GPIO pin states
  • Use SPI to interface with the E-Ink display
  • Transmit images to E-ink display
  • Periodic refresh of E-ink display
  • Create a dashboard to display info I want
    • Daily agenda
    • TODOs

In terms of priority, the first 4 tasks are the most critical. I have the first task done thanks to PiGPIO.jlā€™s support for GPIO which is pretty rad!

The Story So Far! :books:

So, Waveshare actually has a great foundation for interacting with the E-ink display via Python. The files that are most pertinent are:

In fact, I have begun translating some of the code from epdconfig.py and epd_7in5_V2_test.py into a new Julia package that I am calling Jinkies.jl. Jinkies.jl is built upon PiGPIO.jl and is building properly on the Pi.

Jinkies! Julia: E-Ink made Easy!

Obstacles and Questions :warning:

Some of the most outstanding obstacles and questions I have run into are as follows:

Obstacle 1: I have no idea how to get SPI communication working from within Julia. PiGPIO.jlā€™s support for SPI is completely broken; here is an example:

julia> using PiGPIO

julia> p = Pi()
[ Info: Successfully connected!
Pi("localhost", 8888, true, PiGPIO.SockLock(Sockets.TCPSocket(RawFD(17) open, 0 bytes waiting), ReentrantLock(nothing, 0x00000000, 0x00, Base.GenericCondition{Base.Threads.SpinLock}(Base.IntrusiveLinkedList{Task}(nothing, nothing), Base.Threads.SpinLock(0)), (0, 0, 0))), PiGPIO.CallbackThread(PiGPIO.SockLock(Sockets.TCPSocket(RawFD(17) open, 0 bytes waiting), ReentrantLock(nothing, 0x00000000, 0x00, Base.GenericCondition{Base.Threads.SpinLock}(Base.IntrusiveLinkedList{Task}(nothing, nothing), Base.Threads.SpinLock(0)), (0, 0, 0))), PiGPIO.SockLock(Sockets.TCPSocket(RawFD(18) paused, 0 bytes waiting), ReentrantLock(nothing, 0x00000000, 0x00, Base.GenericCondition{Base.Threads.SpinLock}(Base.IntrusiveLinkedList{Task}(nothing, nothing), Base.Threads.SpinLock(0)), (0, 0, 0))), true, true, 0, 0x00000000, Any[]))

julia> h = PiGPIO.spi_open(p, 1, 100000, Int32(0))
ERROR: MethodError: no method matching write(::IOBuffer, ::Int32)
You may have intended to import Base.write

Closest candidates are:
  write(::Pi, ::Any, ::Any)
   @ PiGPIO ~/.julia/packages/PiGPIO/U0QFH/src/pi.jl:458

Stacktrace:
 [1] spi_open(self::Pi, spi_channel::Int64, baud::Int64, spi_flags::Int32)
   @ PiGPIO ~/.julia/packages/PiGPIO/U0QFH/src/spiSerial.jl:92
 [2] top-level scope
   @ REPL[5]:1

I went through a very onerous debugging process for several hours yesterday testing out various versions of Julia (including 1.0, 1.3, 1.5, 1.6, and 1.10) and concluded the code itself is actually flawed. Once I kinda patched spi_open for example, I would run into further problems within PiGPIO.jl. Hereā€™s the code that was involved in the failures:

PiGPIO Problems with SPI

Erroneous code from within spiSerial.jl:

function spi_open(self::Pi, spi_channel, baud, spi_flags=0)
    # I p1 spi_channel
    # I p2 baud
    # I p3 4
    ## extension ##
    # I spi_flags
    extents=IOBuffer()
    write(extents, spi_flags::Cint)
    return _u2i(_pigpio_command_ext(
        self.sl, _PI_CMD_SPIO, spi_channel, baud, 4, extents))
end
function _u2i(x::UInt32)
   v = convert(Int32, x)
   if v < 0
      if exceptions
          error(error_text(v))
     end
   end
   return v
end

Erroneous code from within pi.jl:

"""
Runs an extended pigpio socket command.

 * `sl`: command socket and lock.
 * `cmd`: the command to be executed.
 * `p1`: command parameter 1 (if applicable).
 * `p2`: command parameter 2 (if applicable).
 * `p3`: total size in bytes of following extents
 * `extents`: additional data blocks
"""
function _pigpio_command_ext(sl, cmd, p1, p2, p3, extents, rl=true)
    ext = IOBuffer()
    Base.write(ext, Array(reinterpret(UInt8, [cmd, p1, p2, p3])))
    for x in extents
       write(ext, string(x))
    end
    lock(sl.l)
    write(sl.s, ext)
    msg = reinterpret(Cuint, sl.s)[4]
    if rl
         unlock(sl.l)
    end
    return res
end

So, this led me to looking at @Ronis_BR 's project BaremetalPi.jl which seems to handle SPI interfaces but I have no idea how to really use it and it doesnā€™t seem to work on my Pi:

julia> using BaremetalPi

julia> init_spi("/dev/spidev0.0", max_speed_hz = 1000)

julia> tx_buf = [0x01, 0x80, 0x00]
3-element Vector{UInt8}:
 0x01
 0x80
 0x00

julia> rx_buf = zeros(UInt8, 3)
3-element Vector{UInt8}:
 0x00
 0x00
 0x00

julia> ret = spi_transfer!(1, tx_buf, rx_buf)
3

julia> rx_buf
3-element Vector{UInt8}:
 0x00
 0x00
 0x00

I also took a look at @notinaboat 's PiGPIOC.jl which provides a Clang.jl-based wrapper for pigpio. Again however, I was a bit lost as to how to make this work.

Obstacle 2: I am struggling with translate some of the Python code to Julia. Overall, I would like to be able to translate the following Python code:

    # Translated to Julia
    def digital_write(self, pin, value):
        if pin == self.RST_PIN:
            if value:
                self.GPIO_RST_PIN.on()
            else:
                self.GPIO_RST_PIN.off()
        elif pin == self.DC_PIN:
            if value:
                self.GPIO_DC_PIN.on()
            else:
                self.GPIO_DC_PIN.off()
        elif pin == self.PWR_PIN:
            if value:
                self.GPIO_PWR_PIN.on()
            else:
                self.GPIO_PWR_PIN.off()

    # Translated to Julia
    def reset(self):
        epdconfig.digital_write(self.reset_pin, 1)
        epdconfig.delay_ms(20) 
        epdconfig.digital_write(self.reset_pin, 0)
        epdconfig.delay_ms(2)
        epdconfig.digital_write(self.reset_pin, 1)
        epdconfig.delay_ms(20)   

    # Unsure how to translate this
    def spi_writebyte(self, data):
        self.SPI.writebytes(data)

    # Unsure how to translate this
    def send_command(self, command):
        epdconfig.digital_write(self.dc_pin, 0)
        epdconfig.digital_write(self.cs_pin, 0)
        epdconfig.spi_writebyte([command])
        epdconfig.digital_write(self.cs_pin, 1)

    def send_data(self, data):
        epdconfig.digital_write(self.dc_pin, 1)
        epdconfig.digital_write(self.cs_pin, 0)
        epdconfig.spi_writebyte([data])
        epdconfig.digital_write(self.cs_pin, 1)

Into Julia. Here are methods I have tried so far:

function digital_write(p, pin, value)
    if pin == RST_PIN
        if value
            PiGPIO.write(p, RST_PIN, PiGPIO.ON)
        else
            PiGPIO.write(p, RST_PIN, PiGPIO.OFF)
        end
    elseif pin == DC_PIN
        if value
            PiGPIO.write(p, DC_PIN, PiGPIO.ON)
        else
            PiGPIO.write(p, DC_PIN, PiGPIO.OFF)
        end
    elseif pin == PWR_PIN
        if value
            PiGPIO.write(p, PWR_PIN, PiGPIO.ON)
        else
            PiGPIO.write(p, PWR_PIN, PiGPIO.OFF)
        end
    end
end

function reset(p::Pi)
    digital_write(p, reset_pin, true)
    delay_ms(20)
    digital_write(p, reset_pin, false)
    delay_ms(2)
    digital_write(p, reset_pin, true)
    delay_ms(20)
end

# Unsure how to translate this
function send_command(p, command):
    digital_write(p, DC_PIN, false)
    digital_write(p, CS_PIN, false)
    # Unsure how to translate this
    epdconfig.spi_writebyte([command])
    digital_write(p, CS_PIN, true)
end

The SPI interfacing is the most confusing to me at the moment. However, if I can get spi_writebyte, send_command, and send_data translated properly, then I can keep proceeding as I could do something like this:

self.reset()    
self.send_command(0x06)
self.send_data(0x17)

to properly initialize my display.

Final Thoughts :thought_balloon:

So far, we are making progress, but I am stuck now! Would anyone be able to give some thoughts and guidance on how to best proceed? I was thinking if need be, maybe I should using PiGPIO.jl GPIO communication and BaremetalPi.jl or PiGPIOC.jl for SPI communication?

What do people think? I am going to CC a few folks whose opinions I would love to hear: @notinaboat @Ronis_BR @Sukera @Alexander-Barth @avik @yakir12 @asinghvi17

Cheers and happy holidays :christmas_tree:

~ tcp :deciduous_tree:

17 Likes

It looks like a fun project. Good luck figuring out those technical puzzles. :four_leaf_clover:

1 Like

Do you HAVE to use SPI? What will happen if you used another (perhaps slower) protocol?

2 Likes

Thank you for sharing the information about this nice project.

Unfortunately, there is a lot of untested code in PiGPIO.jl.
Maybe does this commit already help you to go further?

Do not hesitate the fill an issue here.
It is possible that you find other problems along the way.

If there are any ideas how we can improve the testing of such packages on GitHub Action, that would be helpful too.

3 Likes

Hey @Alexander-Barth!

Ah, hm. This is good to know!

It does! Now I get a different error ā€“ I can open up an issue and we can discuss more there.

EDIT: Issue has been opened here discussing this current problem I run into:

Honestly, if you would be willing to help me with my little hobby project, Iā€™d be happy to also use my little Pi as a testing rig to test various Julia Raspberry Pi packages across versions. Thatā€™s the first step I see at the moment. Would that be helpful?

Just going to respond to some other comments/thoughts too in this message:

I donā€™t know ā€“ all the references I see for the E-Ink display use SPI instead of say serial. I just wouldnā€™t know what to do with serial whereas I have at least a bit of an idea from examples using SPI.

Per @vchuravy on BlueSky, this could be a nice suggestion! Hereā€™s the current failing example:

julia> using BaremetalPi

julia> init_spi("/dev/spidev0.0", max_speed_hz = 1000)

julia> tx_buf = [0x01, 0x80, 0x00]
3-element Vector{UInt8}:
 0x01
 0x80
 0x00

julia> rx_buf = zeros(UInt8, 3)
3-element Vector{UInt8}:
 0x00
 0x00
 0x00

julia> ret = spi_transfer!(1, tx_buf, rx_buf)
3

julia> rx_buf
3-element Vector{UInt8}:
 0x00
 0x00
 0x00

It doesnā€™t appear to be receiving anything is the conundrum despite sending a transmission. @Ronis_BR , do you have any suggestions on this front?

Thanks @g-gundam! We will see how far we can get! :smile:

1 Like

I donā€™t know OTOH which chip your Raspberry Pi uses, but if you want to go the route of directly toggling registers instead of going through the virtual device exposed on /dev/spidev0.0, I highly recommend using something like MCUCommon.jl to define various operations on memory mapped registers. Youā€™ll need the memory addresses the registers are mapped to for this to work - you can probably find them in the datasheet. I havenā€™t tried running this yet from a ARM-native julia running on a Pi, but I donā€™t inherently see why this shouldnā€™t work. Check the docstring of @regdef for the main usage. If you have trouble with it, let me know!

This does mean building up an abstraction for the SPI (which isnā€™t something Iā€™ve done in Julia yet either for my experiments - sorry!), but it might be a worthwhile learning experience (though this will probably also take some time to get right). This peripheral description will probably be a good/helpful reference.

How are you checking that something is actually being sent? You can accomplish that by hooking the SPI output up to a breadboard, and connecting that then to your E-Ink display as well as a logic analyzer. Thereā€™s a bunch of knock-offs of the 8-pin Salae floating around on the internet, that are compatible with their good software. Just search for ā€œlogic analyzer 8 channelā€ on Amazon and youā€™ll find them for ~10$.

1 Like

Hi @TheCedarPrince !

I have been using BaremetalPi.jl to communicate to an inertial sensor (gyro) using the SPI interface. However, I have only the Raspberry Pi Zero 2W.

You can see the docs here:

https://ronisbr.github.io/BaremetalPi.jl/stable/man/spi/

I remember struggling with SPI interface because of the different configurations you need to perform.

2 Likes

Hi folks, back with a little update! After some tinkering and help from @Alexander-Barth, PiGPIO.jl is now able to open SPI interfaces properly:

julia> using PiGPIO

julia> p = Pi()
[ Info: Successfully connected!
Pi("localhost", 8888, true, PiGPIO.SockLock(Sockets.TCPSocket(RawFD(17) open, 0 bytes waiting), ReentrantLock(nothing, 0x00000000, 0x00, Base.GenericCondition{Base.Threads.SpinLock}(Base.IntrusiveLinkedList{Task}(nothing, nothing), Base.Threads.SpinLock(0)), (0, 0, 0))), PiGPIO.CallbackThread(PiGPIO.SockLock(Sockets.TCPSocket(RawFD(17) open, 0 bytes waiting), ReentrantLock(nothing, 0x00000000, 0x00, Base.GenericCondition{Base.Threads.SpinLock}(Base.IntrusiveLinkedList{Task}(nothing, nothing), Base.Threads.SpinLock(0)), (0, 0, 0))), PiGPIO.SockLock(Sockets.TCPSocket(RawFD(18) paused, 0 bytes waiting), ReentrantLock(nothing, 0x00000000, 0x00, Base.GenericCondition{Base.Threads.SpinLock}(Base.IntrusiveLinkedList{Task}(nothing, nothing), Base.Threads.SpinLock(0)), (0, 0, 0))), true, true, 0, 0x00000000, Any[]))

julia> h = PiGPIO.spi_open(p, 0, 100000, 0)
0

We are currently in the process of getting writing and reading to work but we have a nice path forward with comparing to the state of the art using the Python pigpio package. It almost seems to work but reading bytes seems to have some flaws as discussed here:

5 Likes

Changing the title to include Raspberry Pi and SPI would make this post more searchable.

Going by memory, when I selected a package for pin toggling (not SPI), on the RPI one of the driving factors was to not have to use sudo for anything. I found BareMetalPI.jl fit the bill nicely (Thanks @Ronis_BR). I currently use the PI 4 and will start using the PI 5.

4 Likes

Latest update: after tinkering and help from @Alexander-Barth , we tentatively have reading and writing working over SPI!

Additionally, it seems like I have no blockers now with translating some of this Python code over to Julia code now using PiGPIO.jl. My first goal will be to translate the clear screen Python function for an e-ink display:

def Clear(self):
    self.send_command(0x10)
    self.send_data2([0xFF] * int(self.width * self.height / 8))
    self.send_command(0x13)
    self.send_data2([0x00] * int(self.width * self.height / 8))

    self.send_command(0x12)
    epdconfig.delay_ms(100)
    self.ReadBusy()

I think there should be no more barriers now. Iā€™ll report back what I am able to do sometime later. For now, back to some other research!

9 Likes