This post is about Tinyterm, a silly hack that I presented as a lightning talk at last month’s Sydney Go User group 1. You can find the original slides online at talks.golang.org.
This talk is about a experiment to see if I could drive I2C devices from Go through my laptop’s VGA port. It was inspired by a recent post on Hack-a-Day.
There are several parts to this presentation. There is some Go in here, trusty me.
The first piece of the puzzle is the I2C bus.
The I2C bus is a low speed two wire serial bus mainly used for connecting sensors and microcontrollers together.
But, you don’t even need a microcontroller to use I2C. If you’re patient you can bit bang the protocol using a few resistors and tack switches.
I2C isn’t just used on microcontrollers like the Arduino. It’s has been used inside every PC and laptop for decades as a slow speed serial protocol for interfacing with simple devices like temperature sensors.
If you’ve used the lmsensors
package in Linux, or have heard of SMBus, this is basically a variant of I2C.
Importantly, I2C is also used as the protocol to detect an external monitor, where it goes under the name DDC2b.
Talking to I2C devices is as simple as installing a kernel module which will create devices entries in your /dev/
directory.
% ls /dev/i2c* /dev/i2c-0 /dev/i2c-1 /dev/i2c-2 /dev/i2c-3 /dev/i2c-4 /dev/i2c-5 /dev/i2c-6 /dev/i2c-7 /dev/i2c-8
Each device on the I2C bus has a unique address. You can use the i2cdetect
command (part of the i2c-utils package on Ubuntu) to scan the bus.
In this example, the device responding at 0x50
is my laptop’s internal LCD screen. That device is an EEPROM which holds specifications of the screen.
After a bit of reverse engineering of the hack a day post, and a quick trip to Jaycar for parts I came up with this simple adapter
The adapter just breaks out pins 5, 9, 12, and 15 to the Dupont patch cables. Using a logic analyser I verified that pins 12 and 15 looked like I2C data when I ran i2cdetect
.
The next step was to connect up a real I2C device to the bus and see if I could detect it with i2cdetect
.
Although both the LCD and the laptop are 5 volt devices I wasn’t sure how much current the laptop could source on pin 9, so I opted to buffer the devices using a Freetronics level shifter which effectively isolates the laptop from the high current LED backlight on the LCD panel.
Now the hardware was done, it was time to write some code. Driving an I2C device from userspace in Go is pretty straight forward; open the device, then use an ioctl
to tell the kernel to bind the file descriptor to a remote I2C device.
The LCD I was using is based on the Hitachi HD44780 standard which has a baroque protocol using many pins and is completely incompatible with I2C.
To interface between the HD44780 I’m using a cheap PCF8574 I2C IO expander which takes any byte received over I2C and maps it directly to its output pins.
I adapted some Python code to work with my Go I2C
type which gave me a set of LCD primitives to work with.
So now I can drive the output of the LCD with Go. Here is an example
But this was kind of boring, could I do something more interesting ?
Looking back through this project it occurred to me that the recurring theme was, in the best UNIX tradition, everything is a file.
- I2C buses are visible in userspace as files
- Each I2C device is a file descriptor, once opened and programmed by ioctl
- UNIX processes talk to each other over file descriptors
- In Go, that is basically an
io.Writer
, right ?
So, could I connect a UNIX process’s output to the LCD screen transparently ?
Enter Tinyterm, a simple Go program that does just that.
Using an lcdWriter
type (more on that in the next slide), Tinyterm spawns a child process and redirects Stdout and Stderr to the LCD.
The lcdWriter
‘s Write
method has a little bit of smarts to deal with making the LCD look like a 16 x 4 terminal, rather than a linear stream of characters, handles scrolling the screen, and obscures the odd addressing scheme of the video memory inside the HD44780.
Putting it all together we have the tinyterm
command, which runs its arguments as a subprocess, sending the child’s stdout and stderr to the LCD. Stdin is not redirected, so it takes input from the original terminal device, eventually mapping back to my keyboard.
The code for the i2c and lcd types is on github, github.com/davecheney/i2c, along with the helloworld and tinyterm example programs.
1 The talk was recorded but it is not clear if the recording worked, I will update this post if/when the video is available.