DEV Community

Cover image for I²C on the Pinephone
Paul
Paul

Posted on

I²C on the Pinephone

Overview

The Pinephone is an open source smartphone that runs Linux. It's a really neat device and will hopefully be suitable as a daily driver smartphone in the near future, but for now it's very pleasant to hack on. Short surface level overview of the device here.

The below example is a little contrived, but hopefully sheds some light on connecting external peripherals to the Pinephone. I'm also fairly new to embedded stuff, D-bus, and still shaky on my Rust knowledge, so critique on a better way to implement all this is very welcome.

I²C

I²C is a common method for interfacing with embedded peripherals. It's a fairly simple communication method. For a deeper dive checkout out the Embedded Rust Discovery Book.

Embedded rust

In order to understand how the driver works we need to understand a little bit about the embedded rust ecosystem. embedded-hal is a set of traits which can be used to create platform-agnostic drivers. The SSD1306 targets embedded-hal, so any hal that implements the embedded-hal traits will be able to use the driver. In order to use SSD1306 driver on our Pinephone we'll take advantage of the linux-embeddedhal crate.

I2c on the pinephone

Accessing the pogo pins

You'll need some way to connect your device to the Pinephone. The easiest method is to just purchase a breakout board. But, there are other ways to access these pins. I used the breakout board from this repo and ordered them from OSH Park. There is some soldering required.

Pinephone back

Connect the display

Connecting the display is pretty straightforward.

One you've connected your breakout you just need to match up the pins (GND VCC SCL SDA) to the display.

I²C on Linux

🚨 You may need to enable the proper kernel driver. I'm using Arch Linux on my pinephone, which appears to have the I²C kernel module already enabled.

Make sure you have have the i2cdetect utility installed. On arch linux the utility is in the i2c-tools package. With that tool installed run i2cdetect -l to print out all the i2c devices:

i2c-3   unknown         mv64xxx_i2c adapter                     N/A
i2c-1   unknown         mv64xxx_i2c adapter                     N/A
i2c-4   unknown         i2c-csi                                 N/A
i2c-2   unknown         mv64xxx_i2c adapter                     N/A
i2c-0   unknown         DesignWare HDMI                         N/A
i2c-5   unknown         i2c-2-mux (chan_id 0)                   N/A
Enter fullscreen mode Exit fullscreen mode

Each I²C device will have an address we use to interface with it. The SSD1306 by default has an address of 3C. In order to figure out which I²C device we need to communicate with, we can probe each of the interfaces. Here is my output after probing device 3

$ i2cdetect 3
 0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f  
00:                         -- -- -- -- -- -- -- --    
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --    
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --    
30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- --    
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --    
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --    
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
Enter fullscreen mode Exit fullscreen mode

/dev/i2c-3 appears to be the interface the SSD1306 display is connected to. With that knowledge we can start programming some Rust!

Rust!

Create a new Rust project. Use rustup if you don't already have it installed.

cargo new i2c-test && cd i2c-test

Modify Cargo.toml to add the dependencies we need:

# ...Omitted

[dependencies]  
ssd1306 = "0.5.2"  
linux-embedded-hal = "0.3.0"  
embedded-graphics = "0.6.2"
Enter fullscreen mode Exit fullscreen mode

And then modify src/main.rs

use embedded_graphics::{  
   fonts::{Font6x8, Text},  
   pixelcolor::BinaryColor,  
   prelude::*,  
   style::TextStyleBuilder,  
};  
use ssd1306::{mode::GraphicsMode, prelude::\*, Builder, I2CDIBuilder};  
use core::fmt::Write;  
use linux_embedded_hal::I2cdev;  

fn main() {  
   let i2c = I2cdev::new("/dev/i2c-3").unwrap(); // Replace with the proper interface!
   let interface = I2CDIBuilder::new().init(i2c);  
   let mut disp: GraphicsMode<_, _> = Builder::new()  
       .size(DisplaySize128x64)  
       .connect(interface).into();  
   disp.init().unwrap();  

   let text_style = TextStyleBuilder::new(Font6x8)  
       .text_color(BinaryColor::On)  
       .build();  

   Text::new("Hello world!", Point::zero())  
       .intostyled(text_style)  
       .draw(&mut disp);  

   disp.flush().unwrap();  

}
Enter fullscreen mode Exit fullscreen mode

Before we run this code let's take a step back and look at the permissions on the i2c-# devices.

$ ls -l /dev/ | grep i2c

crw-------  1 root  root    89,   0 May  4 12:09 i2c-0  
crw-------  1 root  root    89,   1 May  4 12:09 i2c-1  
crw-------  1 root  root    89,   2 May  4 12:09 i2c-2  
crw-------  1 root  root    89,   3 May  4 12:09 i2c-3  
crw-------  1 root  root    89,   4 May  4 12:09 i2c-4  
crw-------  1 root  root    89,   5 May  4 12:09 i2c-5
Enter fullscreen mode Exit fullscreen mode

These devices are both owned by root and in the group root. If we try to run our rust code right now, the code will fail because our user lacks permission to read from the I²C device. A quick fix is to run:

chown $(whoami) /dev/i2c-# (replace # with the I²C # you found from the previous section).

Now we can run: cargo run and you should see the display light up with Hello world!.

IMG_20210504_131234

📝 There isn't anything Pinephone specific about this code. You should be able to run this exact program (provided that it references the right device) on something like a raspberry pi.

Let's do something more elaborate

Wouldn't it be cool, although maybe a little unnecessary if we could display the cell signal on our new OLED screen? Let's do it!

D-bus modem manager

D-bus is a messaging middleware for communicating between multiple processes. What we're interested in is the ModemManager D-bus interface. Take a look at the modemmanager documenation to get an idea of what you can do with this interface. D-Feet is a great little app that can list all the different interfaces you can connect to and all the methods and properties they provide.

D-bus and Rust

Generating our D-bus interface

To actually interact with D-bus we'll generate the rust code necessary using dbus-codegen. This is surprisingly easy once you have all the pieces. First install the utility:

cargo install dbus-codegen

And then run the utility. My command ended up looking something like this:

dbus-codegen-rust -s -g -m None -d org.freedesktop.ModemManager1 -p /org/freedesktop/ModemManager1/Modem/0 > src/modemmanager.rs

Printing out the cell signal level

At this point your rust directory should look like this:

├── Cargo.lock  
├── Cargo.toml  
└── src  
    ├── main.rs  
    └── modemmanager.rs
Enter fullscreen mode Exit fullscreen mode

First, let's update Cargo.toml with the dbus dependency

[dependencies]
dbus = "0.9.2"
# Extra deps omitted!
Enter fullscreen mode Exit fullscreen mode

Now, we can get the signal level with the following code:

mod modemmanager;
//...
// Omitted extra packages!
//...
use dbus::{blocking::Connection, arg};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Notice that we used `new_system`
    // This connects to the `system` dbus
    let conn = Connection::new_system()?;

    // Second, create a wrapper struct around the connection that makes it easy
    // to send method calls to a specific destination and path.
    let proxy = conn.with_proxy("org.freedesktop.ModemManager1", "/org/freedesktop/ModemManager1/Modem/0", Duration::from_millis(5000));

    // Import our generated interface!
    use modemmanager::OrgFreedesktopDBusProperties;

    // Use d-feet to make sure that this is the same on your system.
    // Look for the `SignalQuality` property
    let signal_quality:  Box<dyn arg::RefArg> = proxy.get("org.freedesktop.ModemManager1.Modem", "SignalQuality")?;

    // Cast the signal quality to an i64, yes this has some code smell, but we're just hacking this :)
    // in here
    let signal: i64 = signal_quality
        .as_iter()
        .unwrap()
        .next()
        .unwrap()
        .as_i64()
        .unwrap();

    // ...
    // I2C code from the previous omitted
    // ...
    Ok(())
}

Enter fullscreen mode Exit fullscreen mode

IMG_20210504_173254

What next?

Look at the methods generated by dbus-codegen. Instead of just running this method and closing we could listen for the PropertiesChanged signal to update the display. Or look at the existing embedded hal drivers and pick up some new hardware to play with. Any of the sensors listed there should work just as easily as the SSD1306 display. If you have something which doesn't have a driver in Rust yet, well writing embedded drivers in rust isn't that hard. Give it a try.

The Pinephone may still be a little rough to use as a daily driver, but it has a lot of potential and hackability. I hope more people invest in this device and we start seeing some more interesting projects.

Misc Sources

  1. https://wiki.pine64.org/index.php/PinePhone#Pogo_pins
  2. https://docs.rust-embedded.org/book/portability/index.html
  3. https://github.com/diwic/dbus-rs/blob/master/dbus/examples/argument_guide.md
  4. https://github.com/diwic/dbus-rs
  5. https://www.freedesktop.org/software/ModemManager/doc/latest/ModemManager/

Top comments (0)