As part of a long term project I'm working on for my Raspberry Pi, I wanted a linux kernel driver for the Seiko S4548-AGC display which I have used in a couple of other projects. The display seems rare, I got mine from scrapped phones.
The S4548 is a 101*40 pixel graphic display, controlled over a (poorly implemented) I²C bus. Despite the specific display being of little use to anyone else, the driver is pretty small and straightforward, and it may be a helpful reference for someone trying to implement a linux kernel I²C device driver.
This information is duplicated from the LCD Glasses page, and describes how the display is controlled.
The display pinout is (pin1 is on the right when looking at the back of the display):
Pin | Symbol | Function | Comment |
---|---|---|---|
1 | VLCD | VLCD | VDD-8V - VDD-5V |
2 | VSS | Logic Ground | 0V |
3 | VDD | VDD | 2.4V - 5.5V |
4 | SCL | I2C Clock | ~100kHz (though I managed more) |
5 | SDA | I2C Data | Write address 0x74 |
6 | OSC1 | RC Oscillator input | Connect 470k across OSC2/OSC1 |
7 | OSC2 | RC Oscillator output | |
8 | VDD | VDD | Tie 3 & 8 together externally |
The display's VRAM is split up into 5 pages (rows) of 101 bytes, so that each bit in each byte represents a pixel.
The I2C transaction looks something like this:
S | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | * | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | . | . | . | P |
I2C Address (0x74) | A | Page 0b000 - 0b100 |
Cmd | A | Col Address 0b0000000 - 0b1100100 |
A |
You may notice that all the 'ACK' (A) bits are written as 1, apparently there is an error in the silicon which means the LCD will not generate the correct ACK signal, so that cycle should be ignored (and ACK assumed). The "..." is the display data - here you write as many bytes as you want, which will go into the VRAM sequentially starting at the location specified by Page and Col Address. When the end of a page is reached (i.e you get to column 0x65), the next write will go to the first column of the next page and so on. The bits in the command field 'Cmd' have the following meanings:
MSB | All pixels ON(1)/OFF(0) |
- | Power Save ON(1)/OFF(0) |
LSB | Display ON(1)/OFF(0) |
Power Save has the highest priority and select all pixels has second highest.
The I²C subsystem of the linux kernel separates out busses, algorithms and clients. A bus is as it sounds - an I²C bus which is accessible by the kernel. For instance on the Raspberry Pi, when configured correctly, there are two busses - BSC0 and BSC1, which present themselves as i2c-0 and i2c-1.
A 'bus' uses an algorithm to get bits of data onto the physical i2c bus. This abstraction means that you can have a generalised algorithm which might get used by several busses - such as the bitbang algorithm (i2c-algo-bit.c), which implements a generic bit-banged i2c protocol, requiring no hardware functionality other than setting and reading and SCL and SDA line. It's not always possible to split an algorithm from a bus, for instance if there is a proprietary I²C controller (such as the BSC on the R-Pi) then the bus and algorithm will likely be implemented together in the same file.
To write a driver for a screen or a thermometer or whatever else, you don't have to worry about any of this - that's where clients come in. An i2c client is defined by a struct i2c_driver, which is filled with function pointers to carry out various operations. The prototype for struct i2c_device is in include/linux/i2c.h
As a minimum, you should provide probe and remove functions, which will get called (unsurprisingly) when an instance of your device is attached to a bus. The detect function can be populated if there is a way for you to detect the presence of your device on the bus - for instance check for an ACK at the appropriate address. This can allow for automatic detection of devices. The id_table member is a list of all the devices supported by your driver - that is, if you want your driver to be used when someone tries to attach a foo_device to an I²C bus, you need a relevant entry in id_table. The field is an array of struct i2c_device_ids:
struct i2c_device_id { char name[I2C_NAME_SIZE]; kernel_ulong_t driver_data /* Data private to the driver */ __attribute__((aligned(sizeof(kernel_ulong_t)))); };
static const struct i2c_device_id s4548_id[] = { { "foo_device", 0 }, { } };
The struct i2c_client * which is an argument to all of the driver functions is created by the kernel at I²C device instantiation. It's probably a good idea to define some kind of internal structure which holds all the informatin your driver needs about a particular instance as well as a reference to this struct i2c_client *, you'll need that pointer to carry out bus operations.
So once you have the required boilerplate down you're going to want to actually send some data on the bus. There's a couple of functions to do this, depending on the behaviour you need.
/* * The master routines are the ones normally used to transmit data to devices * on a bus (or read from them). Apart from two basic transfer functions to * transmit one message at a time, a more complex version can be used to * transmit an arbitrary number of messages without interruption. * @count must be be less than 64k since msg.len is u16. */ extern int i2c_master_send(const struct i2c_client *client, const char *buf, int count); extern int i2c_master_recv(const struct i2c_client *client, char *buf, int count); /* Transfer num messages. */ extern int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
These functions are implemented and exported by a bus/algorithm and can thus be used by a client on any bus (providing the bus supports I²C). The i2c_master_send() and i2c_master_recv() functions provide the most basic operations, sending from or receiving into a buffer of chars. The i2c_transfer() function lets you transfer sets of messages - where each message contains a slave address, flags defining direction and other bus parameters, and a data buffer.
struct i2c_msg { __u16 addr; /* slave address */ __u16 flags; #define I2C_M_TEN 0x0010 /* this is a ten bit chip address */ #define I2C_M_RD 0x0001 /* read data, from slave to master */ #define I2C_M_NOSTART 0x4000 /* if I2C_FUNC_PROTOCOL_MANGLING */ #define I2C_M_REV_DIR_ADDR 0x2000 /* if I2C_FUNC_PROTOCOL_MANGLING */ #define I2C_M_IGNORE_NAK 0x1000 /* if I2C_FUNC_PROTOCOL_MANGLING */ #define I2C_M_NO_RD_ACK 0x0800 /* if I2C_FUNC_PROTOCOL_MANGLING */ #define I2C_M_RECV_LEN 0x0400 /* length will be first received byte */ __u16 len; /* msg length */ __u8 *buf; /* pointer to msg data */ };
On top of the I²C driver code, you will need standard kernel module code - an init and exit function, author and license definitions and any other bits and pieces you want to add. There are plenty of websites and books documenting the general form of a kernel module, I found Linux Device Drivers, 3rd Edition invaluable.
Other resources focussing on I²C drivers specifically are writing-clients in the kernel Documentation/i2c directory, this article on embedded-bits and this article on linuxjournal, as well as many, many others which you can easily find through google.
My S4548 driver presents a char device to provide access to the screen. This character device (/dev/s4548-N) lets you read, write or memory-map a framebuffer which gets pushed to the screen at a rate set by the s4548_rate module parameter (default 30 Hz). There is also an IOCTL, which will simply send the argument number to the display as a command byte. This allows access to the on/off, power save and all-segments-on functionality.
The driver stores a struct s4548 for each screen instance, which holds the framebuffer, configuration flags and a pointer to the associated i2c_client. It also holds the delayed_work structure used for the screen update workqueue. The driver creates a device class for all S4548 devices to use, and allocates a number of device minor numbers (set to 5) for use by screens.
The code is available on bitbucket: git clone https://bitbucket.org/kernelcode/s4548.gitOr from the links on the left if you would prefer.
You can access the framebuffer directly through the character device, though this is not particularly useful as it's binary display data which you need to write. This shows some example commands to attach a screen to an I²C bus, fill it with random data and then clear it again:
% echo s4548 0x3A > /sys/class/i2c-adapter/i2c-0/new_device # attach to i2c-0 at address 0x3A % cat /dev/urandom > /dev/s4548-0 # should display random noise % cat /dev/zero > /dev/s4548-0 # should clear the display
Note that you will either need to be root to run these commands, or change the permissions on the device node appropriately. I use a udev rule to automatically give members of plugdev write access and display a message when a display is added:
$ cat /etc/udev/rules.d/15-s4548.rules ACTION=="add", SUBSYSTEM=="s4548", GROUP="plugdev", MODE="0664", RUN+="/home/kernelcode/.util/bootmsg.sh %k"
To display text on the screen, you need to convert the string into appropriate binary data to be displayed. I wrote a program to handle the text-to-binary conversion, as well as word-wrapping to make the text display nicely on the screen. To anyone who's never tried, writing the word-wrapping code was like stabbing myself in the face for a day, but eventually I got it to work.
The code is again on bitbucket: git clone https://bitbucket.org/kernelcode/bin_fonts.git
Or on the left.
$ ./generate_text Takes exactly one argument - a string to print Outputs a binary stream representing the input string $ ./generate_text "Hello, World :)" > /dev/s4548-0
As mentioned in the screen section, the S4548 never generates an ACK bit. The linux I²C driver model has functionality to support this through I2C_FUNC_PROTOCOL_MANGLING, and the flag I2C_M_IGNORE_ACK which can be added to an i2c_msg. Unfortunately the i2c-bcm2708 driver does not support protocol mangling, and my attempts to add it were unsuccessful. I think the BSC hardware automatically ends the transaction when a bus error occurs, and I was unable to override this behaviour. As the R-Pi peripherals datasheet is not explicit on this I can't be sure
As a solution, in order to make the screen work on my R-Pi, I added an Attiny2313 which sits on the I²C bus. It listens to all transactions, and if it detects a message intended for the S4548, then it generates an ACK at the appropriate time. Note that the Attiny isn't doing anything with the data, it just sits on the bus in parallel sniffing all the data that goes by. My project will eventually need an AVR anyway to carry out some human interface functions, so I will just add the ACK code to the ACR in the finished system, and thus the parts count does not increase.
The AVR is also used to generate the negative voltage required by the display. It generates a square wave using a PWM channel, which is used as the input to a 2-diode, 2-capacitor inverter circuit. (Effectively replacing the 555 in this circuit). This provides around -4.3 V, which is fine for my purposes.