Raspberry PI, GfxLCD and ILI9486

Recently I bought an LCD 4″ IPS with an ILI9486 chip from Waveshare. It should be better than 2.8″ πŸ™‚ In instruction states that this display can work as system screen and I thought that maybe we can use PyQt or something instead of a custom driver.
The idea died quickly. I tried to run it on Raspberry Pi Zero W and this is not trivial πŸ™‚ First, I hit the bug with inability to use GPIO #17 and driver couldn’t be loaded. After some googling, I know that this is caused by the newest kernel. So I reverted to old one and… screen booted up but WiFi and Bluetooth did not.
So this way is no go. Back to the original idea and custom driver for ILI9486.

Planning

This screen uses SPI bus on both, display and touch panel. So we need to use CS signal to control which device is active.
This is a little problem because our lib doesn’t support this. The first thing to do is to add such ability.

From what I see in documentation and in Raspbian driver workflow is similar to older ILI9325. It also operate on area and also has many colours πŸ™‚
What we need is a new driver with init function.
But I found another problem, or the bug πŸ™‚ In drawing/area we are using driver directly. It shouldn’t be like that, function _set_area should be in the base driver. The second thing to fix.

AD7846

Surprise! Let’s begin with a touch panel. From what I see we should be able to use the existing driver for AD7843 with some changes.
We need to add cs_pin to the constructor and use it in _interrupt function:

    def _interrupt(self, channel):
        """call users callback"""
        if self.cs_pin:
            RPi.GPIO.output(self.cs_pin, 0)
        self.callback(self.get_position())
        if self.cs_pin:
            RPi.GPIO.output(self.cs_pin, 1)

Yes, CS can be used only with a callback, not with a reading position in a loop. But this is not a big problem, if you want to use a loop, set pin low manually.
With only this few changes, the demo works somehow.

def interrupt(position):
    print("click", position)
touch = AD7843(320, 480, 17, interrupt, 7)
touch.init()

Default wiring for CS is pin #7 and for an interrupt is #17.

There is still calibration to do but let’s focus on this later.

ILI9325

Here, we need to add a CS pin to the driver, and we can add a LED pin too. And use them both when needed:) The LED pin will be enabled in init function so we have a backlight. As for the CS, we use this only in send function.
Now we can move function _set_area to chip class from area class.

ILI9486

We are ready for the main dish, a huge 320×480 IPS screen. We need to write a driver class and a chip class.
Let’s begin with the driver. It is an SPI one.

We need to know on which bus we operate and what is the wiring of the RS, RST and CS. As we have a Waveshare shield we have no way of changing default pins in an easy way. They are wired to pins: 24, 25 and 8.
We have no control over backlight but I’m leaving the LED pin, Waveshare is not the only kit wth ILI9486.
Also, CS is not mandatory, if we have only the screen we can wire it to the ground.
Whole SPI driver:

class SPI(Driver):
    """SPI communication driver"""
    def __init__(self, spi=0, speed=2000000):
        self.pins = {
            'CS_LCD': 8,
            'RST': 25,
            'RS': 24,
            'LED': None
        }
        self.spi = spidev.SpiDev()
        self.spi.open(spi, 0)
        self.spi.max_speed_hz = speed
        self.spi.mode = 0

    def init(self):
        """initialize pins"""
        for pin in self.pins:
            if self.pins[pin] is not None:
                RPi.GPIO.setup(self.pins[pin], RPi.GPIO.OUT)
                RPi.GPIO.output(self.pins[pin], 0)

        if self.pins['CS']:
            RPi.GPIO.output(self.pins['CS_LCD'], 1)

        if self.pins['LED']:
            RPi.GPIO.output(self.pins['LED'], 1)

    def reset(self):
        """reset a display"""
        if self.pins['LED']:
            RPi.GPIO.output(self.pins['LED'], 1)
        if self.pins['CS']:
            RPi.GPIO.output(self.pins['CS'], 1)
        RPi.GPIO.output(self.pins['RST'], 1)
        time.sleep(0.005)
        RPi.GPIO.output(self.pins['RST'], 0)
        time.sleep(0.005)
        RPi.GPIO.output(self.pins['RST'], 1)
        time.sleep(0.005)

    def cmd(self, data, enable):
        """send command to display"""
        RPi.GPIO.output(self.pins['RS'], 0)
        if self.pins['CS']:
            RPi.GPIO.output(self.pins['CS_LCD'], 0)
        self.spi.xfer2([data])
        if self.pins['CS']:
            RPi.GPIO.output(self.pins['CS_LCD'], 1)

    def data(self, data, enable):
        """send data to display"""
        RPi.GPIO.output(self.pins['RS'], 1)
        if self.pins['CS']:
            RPi.GPIO.output(self.pins['CS_LCD'], 0)
        self.spi.xfer2([data])
        if self.pins['CS']:
            RPi.GPIO.output(self.pins['CS_LCD'], 1)

The function reset and init are the same as in older ILI.

It’s time for chip class. I had lots of problems with good init procedure but I think we have it right. It is good to look into a Linux drivers sometimes πŸ™‚ But before I could refactor and clean up an init procedure bad thing happened…

Kab00m

This was terrible, my plug strip reacted to some anomaly and fuse burned. One RPi is down and my new shiny screen is damaged :/

What now?

To see if this can be fixed I installed screen on older RPi(v1 B+) and installed official drivers for it. And to my surprise, it started as it should at the beginning, contrary to problems on Pi Zero W. But the picture is still highly distorted. Bah, hardware is dead 😦

I hoped for some kind of reset when used with original drivers but no go. Screen damaged badly. Bah, gonna cry πŸ™‚

So what now?
Seems we can do some more progress but it won’t be perfect especially colours. We may need new functions to convert 888 RGB to displays format (I think it is 565). Is same as ILI9325 but will it work?

So let’s move on…

Chip class

Let’s try and do as much as possible. For sure we can write an init and _set_area functions.

"""ILI9486 chip driver"""
import time
from gfxlcd.drawing.area import Area
from gfxlcd.abstract.chip import Chip


class ILI9486(Area, Chip):
    """Class for ILI9486 based LCD"""
    def __init__(self, width, height, driver):
        Chip.__init__(self, width, height, driver, True)
        Area.__init__(self, driver)

    def _converted_background_color(self):
        """color from 8-8-8 to 5-6-5"""
        rgb = self.options['background_color']['R'] << 16 | \
            self.options['background_color']['G'] << 8 | \ self.options['background_color']['B'] return ((rgb & 0x00f80000) >> 8) |\
            ((rgb & 0x0000fc00) >> 5) | ((rgb & 0x000000f8) >> 3)

    def _converted_color(self):
        """color from 8-8-8 to 5-6-5"""
        rgb = self.options['color']['R'] << 16 | \
            self.options['color']['G'] << 8 | \ self.options['color']['B'] return ((rgb & 0x00f80000) >> 8) |\
            ((rgb & 0x0000fc00) >> 5) | ((rgb & 0x000000f8) >> 3)

We are using colour converter from older ILI but I do not know if it works correctly. If I get my hands on another new display I will fix this at once.

    def init(self):
        """init display"""
        self.driver.init()
        Area.init(self)
        Chip.init(self)
        self.driver.reset()

        self.driver.cmd(0x0b, None)
        self.driver.data(0x00, None)

        self.driver.cmd(0x11, None)

        self.driver.cmd(0x3a, None)
        self.driver.data(0x55, None)

        self.driver.cmd(0x36, None)
        self.driver.data(0x28, None)

        self.driver.cmd(0xc2, None)
        self.driver.data(0x44, None)

        self.driver.cmd(0xc5, None)
        self.driver.data(0x00, None)
        self.driver.data(0x00, None)
        self.driver.data(0x00, None)
        self.driver.data(0x00, None)

        self.driver.cmd(0xe0, None)
        self.driver.data(0x0F, None)
        self.driver.data(0x1F, None)
        self.driver.data(0x1C, None)
        self.driver.data(0x0C, None)
        self.driver.data(0x0F, None)
        self.driver.data(0x08, None)
        self.driver.data(0x48, None)
        self.driver.data(0x98, None)
        self.driver.data(0x37, None)
        self.driver.data(0x0A, None)
        self.driver.data(0x13, None)
        self.driver.data(0x04, None)
        self.driver.data(0x11, None)
        self.driver.data(0x0D, None)
        self.driver.data(0x00, None)

        self.driver.cmd(0xe1, None)
        self.driver.data(0x0F, None)
        self.driver.data(0x32, None)
        self.driver.data(0x2E, None)
        self.driver.data(0x0B, None)
        self.driver.data(0x0D, None)
        self.driver.data(0x05, None)
        self.driver.data(0x47, None)
        self.driver.data(0x75, None)
        self.driver.data(0x37, None)
        self.driver.data(0x06, None)
        self.driver.data(0x10, None)
        self.driver.data(0x03, None)
        self.driver.data(0x24, None)
        self.driver.data(0x20, None)
        self.driver.data(0x00, None)

        self.driver.cmd(0xe2, None)
        self.driver.data(0x0F, None)
        self.driver.data(0x32, None)
        self.driver.data(0x2E, None)
        self.driver.data(0x0B, None)
        self.driver.data(0x0D, None)
        self.driver.data(0x05, None)
        self.driver.data(0x47, None)
        self.driver.data(0x75, None)
        self.driver.data(0x37, None)
        self.driver.data(0x06, None)
        self.driver.data(0x10, None)
        self.driver.data(0x03, None)
        self.driver.data(0x24, None)
        self.driver.data(0x20, None)
        self.driver.data(0x00, None)

        self.driver.cmd(0x11, None)
        self.driver.cmd(0x29, None)

Another ‘short’ init sequence πŸ™‚

    def _set_area(self, pos_x1, pos_y1, pos_x2, pos_y2):
        """select area to work with"""
        self.driver.cmd(0x2a, None)
        self.driver.data(pos_x1 >> 8, None)
        self.driver.data(pos_x1 & 0xff, None)
        self.driver.data(pos_x2 >> 8, None)
        self.driver.data(pos_x2 & 0xff, None)
        self.driver.cmd(0x2b, None)
        self.driver.data(pos_y1 >> 8, None)
        self.driver.data(pos_y1 & 0xff, None)
        self.driver.data(pos_y2 >> 8, None)
        self.driver.data(pos_y2 & 0xff, None)
        self.driver.cmd(0x2c, None)

This is all that chip class require.

Next, we need to add demos for this display. The best way is to duplicate ILI ones and swap drivers.

LCD orientation

Those lines:

self.driver.cmd(0x36, None)
self.driver.data(0x28, None)

sets display orientation. But what if we want to have it in a different way?

Okey, this may be useful. We need to declare codes for each direction:

rotations = {0: 0x80, 90: 0xf0, 180: 0x40, 270: 0x20}

set some default in __init__ and set command to:

self.driver.cmd(0x36, None)
self.driver.data(self.rotations[self.rotation], None)

With this, before the call to function init, we may set the desired rotation. But this works only with ILI9486, what about ILI9326?

The case is not so simple. It is easy to rotate by 180 degrees but 90 and 270 require some software modification.
First, we need to declare options for rotations:

    rotations = {
        0: {
            'output': 0x0100,
            'mode': 0x1038,
            'output2': 0xa700
        },
        90: {
            'output': 0x0000,
            'mode': 0x1038,
            'output2': 0xa700
        },
        180: {
           'output': 0x0000,
           'mode': 0x1038,
           'output2': 0x2700
        },
        270: {
            'output': 0x0100,
            'mode': 0x1038,
            'output2': 0x2700
        }
    }

Next, change three commands:

(..)
self.driver.cmd(0x0001, None)
self.driver.data(self.rotations[self.rotation]['output'], None)
(..)
self.driver.cmd(0x0003, None)
self.driver.data(self.rotations[self.rotation]['mode'], None)
(..)
self.driver.cmd(0x0060, None)
self.driver.data(self.rotations[self.rotation]['output2'], None)

Finally, we need to tweak _set_area:

 def _set_area(self, pos_x1, pos_y1, pos_x2, pos_y2):
        """select area to work with"""
        if self.rotation == 90 or self.rotation == 270:
            pos_x1, pos_y1, pos_x2, pos_y2 = pos_y1, pos_x1, pos_y2, pos_x2
(..)

Seems that it works. We will see later with more advanced samples πŸ™‚

To keep compatibility we need to upgrade SSD and NJU driver but not this time:) What we gonna do is just to add self.rotation=0 to both to fake orientation.

Colours

Hurray, new LCD is here, we can fix colours! You may ask what is wrong with them? My answer:

I suspect that we set RGB->BGR. Let’s try and change it… no this not it. So what can it be? From what I see in documentation converter should work. No idea what is going on:/
After 4 hours I went to sleep. And in the morning in came to me! We are sending only 8 bits! not 16!
Fix was in SPI, data function. Instead of

self.spi.xfer2([data])

we need

self.spi.xfer2([data >> 8, data])

And it was almost ok, last change we are using BGR, and we need RGB, so change rotations variable to:

rotations = {0: 0x88, 90: 0xf8, 180: 0x48, 270: 0x28}

And the image is displayed perfectly πŸ™‚

Summary

We made a nice upgrade to our package. It now supports the ILI9486 display. And additionally, we have better control over CS pins.
We also added ability to set an orientation.
But we have an unfriendly situation to solve, a lock mechanism. See that when you touch the screen and position are read, the painting goes crazy.
And we ignored a touch panel. So stay tuned for the next part πŸ™‚

Advertisements

One comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s