Copyright Brian Starkey 2012-2014
Valid HTML 4.01 Transitional

Valid CSS!
iconS-4548ACG Kernel Drivermax, min, close
16th September 2012
Overview(top)

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.


S4548-AGCS4548-AGC
The Screen(top)

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):

PinSymbolFunctionComment
1VLCDVLCDVDD-8V - VDD-5V
2VSSLogic Ground0V
3VDDVDD2.4V - 5.5V
4SCLI2C Clock~100kHz (though I managed more)
5SDAI2C DataWrite address 0x74
6OSC1RC Oscillator inputConnect 470k across OSC2/OSC1
7OSC2RC Oscillator output
8VDDVDDTie 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:

MSBAll pixels ON(1)/OFF(0)
-Power Save ON(1)/OFF(0)
LSBDisplay ON(1)/OFF(0)

Power Save has the highest priority and select all pixels has second highest.

I²C Driver(top)

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))));
};
The array should be terminated by a blank entry, something like this:
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.


S4548 Driver(top)
Obligatory OutputObligatory Output

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.git
Or from the links on the left if you would prefer.

Utilities(top)

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"
            
Word-wrapping in actionWord-wrapping in action

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
            

Raspberry-Pi(top)

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.


Video(top)
iconVideo Playermax, min, close
This video just shows a demonstration of the functionality I have implemented so far. A further write-up of the userspace software which makes this happen will be up in the near future