GfxLCD and ILI9486 C driver – part 3

This time we will add an ILI9486 via SPI support to our C code. And while doing it we will refactor structure and clean up the code (or do more mess :P).
I was thinking that C wrapper should be part of GfxLCD package but maybe it is better to create it as a separate module? We will see but this time the code is available @GitHub.

GFXLCD AND ILI9325 C DRIVER – PART 2
GFXLCD AND ILI9325 C DRIVER – PART 1

Planning

Our goal is to add ILI9486 support and optimize proxy files. But to get there, we need to extract all common functions from proxy _cili9325.c to _cili.c. With this change, we can use them in _cili9486.c

As this LCD works via SPI we need to add SPI support and refactor interface to support both SPI and GPIO. I must say that ILI9486 via SPI is my main goal. This screen is big enough for my ideas and if we get a decent refresh rate via SPI than I’m gonna be very happy.

Refactor

Let’s get to work. All functions that are same for both ILIs will go to the_cili.c and _cili.h files from _cili9325.
So in _cili9325, we will have only functions for this particular display type.

Refactoring was simple, remove the static word from moved functions/variables and leave only:

#include <Python.h>
#include "cili9325.h"
#include "_cili.h"

static char module_docstring[] =
    "GfxLCD ILI9325 in C";

static PyTypeObject ILIType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "cili9325.ili",             /* tp_name */
    sizeof(ILIObject),          /* tp_basicsize */
    0,                          /* tp_itemsize */
    (destructor)ili_dealloc,    /* tp_dealloc */
    0,                          /* tp_print */
    0,                         /* tp_getattr */
    0,                         /* tp_setattr */
    0,                         /* tp_reserved */
    0,                         /* tp_repr */
    0,                         /* tp_as_number */
    0,                         /* tp_as_sequence */
    0,                         /* tp_as_mapping */
    0,                         /* tp_hash  */
    0,                         /* tp_call */
    0,                         /* tp_str */
    0,                         /* tp_getattro */
    0,                         /* tp_setattro */
    0,                         /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT |
        Py_TPFLAGS_BASETYPE,   /* tp_flags */
    module_docstring,          /* tp_doc */
    0,                         /* tp_traverse */
    0,                         /* tp_clear */
    0,                         /* tp_richcompare */
    0,                         /* tp_weaklistoffset */
    0,                         /* tp_iter */
    0,                         /* tp_iternext */
    module_methods,            /* tp_methods */
    0,                         /* tp_members */
    ili_getset,                /* tp_getset */
    0,                         /* tp_base */
    0,                         /* tp_dict */
    0,                         /* tp_descr_get */
    0,                         /* tp_descr_set */
    0,                         /* tp_dictoffset */
    (initproc)ili_init,        /* tp_init */
    0,                         /* tp_alloc */
    ili_new,                    /* tp_new */
};

PyMODINIT_FUNC PyInit_cili9325(void) {
    PyObject *module;

    if (PyType_Ready(&ILIType) < 0)
        return NULL;

    static struct PyModuleDef moduledef = {
        PyModuleDef_HEAD_INIT,
        "cili9325",
        module_docstring,
        -1,
        NULL, //module_methods,
        NULL,
        NULL,
        NULL,
        NULL
    };
    module = PyModule_Create(&moduledef);
    if (!module) return NULL;

    Py_INCREF(&ILIType);
    PyModule_AddObject(module, "ili", (PyObject *)&ILIType);

    return module;
}

in _cili9325.c.

Move content from _cili9325.h to ili.h, update includes in other files and remove it.

We are ready to introduce 9486 support!

ILI9486

Screen with ili9486 is attached to different RPi, to simplify code sharing I mounted there a directory from RPi with 9325.
This way I have one editor for both RPIs. And can easily compile the code.

From what I see we have only one challenge ahead. It’s detection and usage SPI or GPIO based on the constructor. This implies that we need to compile code for both interfaces in one package. Currently, we only compile with gpio.c.

Ok, but this later:) First let’s create a wrapper around ili9486 and SPI as a separate file.

Copy _cili9325 to _cili9486 and replace all occurrences of 9325 with 9486 🙂 With this, we have a basic definition for Python class.

Next, create cili9486.c with:

#include <wiringPi.h>
#include <stdint.h>
#include <stdio.h>
#include "ili.h"
#include "interface.h"

void set_area(ILIObject *self, uint16_t pos_x1, uint16_t pos_y1, uint16_t pos_x2, uint16_t pos_y2) {
    cmd(self, 0x2a);
    data(self, pos_x1 >> 8);
    data(self, pos_x1 & 0xff);
    data(self, pos_x2 >> 8);
    data(self, pos_x2 & 0xff);
    cmd(self, 0x2b);
    data(self, pos_y1 >> 8);
    data(self, pos_y1 & 0xff);
    data(self, pos_y2 >> 8);
    data(self, pos_y2 & 0xff);
    cmd(self, 0x2c);
}

void init_display(ILIObject *self){

}

And the first problem. In init_display we use digitalWrite so we operate on a device, not on the abstraction. Why it is important?
We need to have call like set CS high in this place but how it is made that CS is hight must be in other place.
We need to add functions for setting CS and RST high/low to interface and use them here.

In interface.h add:

void set_cs(ILIObject *self, int state);
void set_rst(ILIObject *self, int state);

and in gpio.c:


void set_cs(ILIObject *self, int state) {
    digitalWrite (self->CS, state);
};

void set_rst(ILIObject *self, int state) {
    digitalWrite (self->RST, state);
}

It would be nice if can quickly check if we are useing GPIO or SPI. Let’s add one line into _cili.c:

self->SPI = -1;

This line should go into the if with TupleSize=14.
Now, we can check SPI variable and we know when we use it (>-1) or when we use GPIO (=-1).

The quick change in setup_pins function:

void setup_pins(ILIObject *self) {
    wiringPiSetupGpio();
    if (self->SPI == -1) {
        pinMode (self->W, OUTPUT);
        pinMode (self->DB8, OUTPUT);
        pinMode (self->DB9, OUTPUT);
        pinMode (self->DB10, OUTPUT);
        pinMode (self->DB11, OUTPUT);
        pinMode (self->DB12, OUTPUT);
        pinMode (self->DB13, OUTPUT);
        pinMode (self->DB14, OUTPUT);
        pinMode (self->DB15, OUTPUT);

        digitalWrite (self->W, LOW);
        digitalWrite (self->DB8, LOW);
        digitalWrite (self->DB9, LOW);
        digitalWrite (self->DB10, LOW);
        digitalWrite (self->DB11, LOW);
        digitalWrite (self->DB12, LOW);
        digitalWrite (self->DB13, LOW);
        digitalWrite (self->DB14, LOW);
        digitalWrite (self->DB15, LOW);

    } else {

    }
    pinMode (self->RS, OUTPUT);
    pinMode (self->RST, OUTPUT);
    pinMode (self->CS, OUTPUT);
    digitalWrite (self->RS, LOW);
    digitalWrite (self->RST, LOW);
    digitalWrite (self->CS, HIGH);
}

Initialization of the interfaces has been separated for SPI and GPIO.

Back to cili9486.c and init_display:

void init_display(ILIObject *self){
    setup_pins(self);
    set_cs(self, HIGH);
    set_rst(self, HIGH);
    delay(5);
    set_rst(self, LOW);
    delay(5);
    set_rst(self, HIGH);
    delay(1);

    //# Read Display MADCTL
    cmd(self, 0x0b);
    data(self, 0x00);
    data(self, 0x00);

    //# Sleep OUT
    cmd(self, 0x11);

    //# Interface Pixel Format
    cmd(self, 0x3a);
    data(self, 0x55); //#0x66 5-6-5 / 55 6-6-6

    //# Memory Access Control (
    cmd(self, 0x36);
    data(self, 0x88);

    //# Power Control 3 (For Normal Mode)
    cmd(self, 0xc2);
    data(self, 0x44);

    //# VCOM Control
    cmd(self, 0xc5);
    data(self, 0x00);
    data(self, 0x00);
    data(self, 0x00);
    data(self, 0x00);

    //# PGAMCTRL(Positive Gamma Control)
    cmd(self, 0xe0);
    data(self, 0x0F);
    data(self, 0x1F);
    data(self, 0x1C);
    data(self, 0x0C);
    data(self, 0x0F);
    data(self, 0x08);
    data(self, 0x48);
    data(self, 0x98);
    data(self, 0x37);
    data(self, 0x0A);
    data(self, 0x13);
    data(self, 0x04);
    data(self, 0x11);
    data(self, 0x0D);
    data(self, 0x00);

    //# NGAMCTRL (Negative Gamma Correction)
    cmd(self, 0xe1);
    data(self, 0x0F);
    data(self, 0x32);
    data(self, 0x2E);
    data(self, 0x0B);
    data(self, 0x0D);
    data(self, 0x05);
    data(self, 0x47);
    data(self, 0x75);
    data(self, 0x37);
    data(self, 0x06);
    data(self, 0x10);
    data(self, 0x03);
    data(self, 0x24);
    data(self, 0x20);
    data(self, 0x00);

    //# Digital Gamma Control 1
    cmd(self, 0xe2);
    data(self, 0x0F);
    data(self, 0x32);
    data(self, 0x2E);
    data(self, 0x0B);
    data(self, 0x0D);
    data(self, 0x05);
    data(self, 0x47);
    data(self, 0x75);
    data(self, 0x37);
    data(self, 0x06);
    data(self, 0x10);
    data(self, 0x03);
    data(self, 0x24);
    data(self, 0x20);
    data(self, 0x00);

    //# Sleep OUT
    cmd(self, 0x11);

    //# Display ON
    cmd(self, 0x29);

    delay(100);
}

Next a dummy spi.c:

#include <wiringPi.h>
#include <stdint.h>
#include "ili.h"

void send(ILIObject *self, uint16_t data) {

}

void cmd(ILIObject *self, uint16_t data) {
    digitalWrite(self->RS, LOW); send(self, data);
}

void data(ILIObject *self, uint16_t data) {
    digitalWrite(self->RS, HIGH); send(self, data);
}

void set_cs(ILIObject *self, int state) {
    digitalWrite (self->CS, state);
}

void set_rst(ILIObject *self, int state) {
    digitalWrite (self->RST, state);
}

We can try and compile two libs, one for ili9325 and other for ili9486. This requires small changes in setup.py:

from distutils.core import setup, Extension

setup(
    name="ili9325",
    ext_modules=[
        Extension("cili9325", ["_cili9325.c", "_cili.c", "ili.c", "cili9325.c", "gpio.c"],  extra_link_args=['-lwiringPi']),
        Extension("cili9486", ["_cili9486.c", "_cili.c", "ili.c", "cili9486.c", "spi.c"],  extra_link_args=['-lwiringPi']),
    ],
)

Compile with:

python3 setup.py build_ext -i

and no errors. Good for us.

Time to take another step, SPI initialization in setup_pins() function:

    } else {
        self->spi_fd = wiringPiSPISetup(self->SPI, self->SPEED);
    }

We are using WiringPi wrapper for SPI interface. Add body to function send in spi.c:

unsigned char buffer[1];
int result;

void send(ILIObject *self, uint16_t data) {
    buffer[0] = data;
    result = wiringPiSPIDataRW(self->SPI, buffer, 1);    
}

Uff.
Let’s compile this and try to use:

import RPi.GPIO  # pylint: disable=I0011,F0401
import cili9325
import cili9486
RPi.GPIO.setmode(RPi.GPIO.BCM)
RPi.GPIO.setup(6, RPi.GPIO.OUT)
RPi.GPIO.output(6, 1)


#lcd = cili9325.ili(320, 240, 18, 27, 17, 25, 22, 23, 24, 5, 12, 16, 20, 21)

# width, height, SPI, SPEED, CS, RST, RS
lcd = cili9486.ili(320, 480, 0, 3200000, 8, 25, 24)
..

Same drawing content as with other ILI display. Run and… works but slowly!
What the heck?

pi@rpiv2:/workspace/workspace/perf $ time python3 test.py
test.py:5: RuntimeWarning: This channel is already in use, continuing anyway.  Use GPIO.setwarnings(False) to disable warnings.
  RPi.GPIO.setup(6, RPi.GPIO.OUT)
320 480

real    0m6.225s
user    0m0.870s
sys     0m3.500s

What is going on? I tried different SPI speeds and no difference…something fishy is going on.
Another problem to solve. And huge one because all my hope was in quick SPI interface :/

Rotation

Let’s do a side task and add rotation. I need to take a break from problems 🙂
First, add rotation variable to our structure:

typedef struct {
..
 int rotation;
} ILIObject;

Next, add declaration for setter and getter via @property

PyObject *magic_get_rotation(ILIObject *self, void *closure);
int *magic_set_rotation(ILIObject *self, PyObject *value, void *closure);

..
    {"rotation", (getter)magic_get_rotation, (setter)magic_set_rotation, 'rotation', NULL},
..

In ili_init add default value for rotation:

self->rotation = 0;

and somewhere below:

PyObject *magic_get_rotation(ILIObject *self, void *closure) {
    return Py_BuildValue("i", self->rotation);
}

int *magic_set_rotation(ILIObject *self, PyObject *value, void *closure) {
    int r = PyLong_AsLong(value);
    self->rotation = r;

    return 0;
}

The code can compile and we can use get/set for rotation but it has no effect. Let’s fix it:)
In cili9325.c at the beginning of init_display declare:

    int rotation_output = 0x100;
    int rotation_mode = 0x1030;
    int rotation_output2 = 0xa700;

    if (self->rotation == 90) {
        rotation_output = 0x0000;
        rotation_mode = 0x1038;
    } else if (self->rotation == 180) {
        rotation_output = 0x0000;
        rotation_output2 = 0x2700;
    } else if (self->rotation == 270) {
        rotation_mode = 0x1038;
        rotation_output2 = 0x2700;
    }

Next, replace proper commands with:

..
cmd(self, 0x0001);
data(self, rotation_output);
..
// set GRAM write direction and BGR=1
cmd(self, 0x0003);
data(self, rotation_mode);
..
// Gate Scan Line
cmd(self, 0x0060);
data(self, rotation_output2);
..

Ok. Rotation for ili9325 works. Time for 9486, its init_display gets a few new lines:

void init_display(ILIObject *self){
    int rotate = 0x88;
    
    if (self->rotation == 90) {
        rotate = 0xf8;
    } else if (self->rotation == 180) {
        rotate = 0x48;
    } else if (self->rotation == 270) {
        rotate = 0x28;
    }
..
    //# Memory Access Control (
    cmd(self, 0x36);
    data(self, rotate);
..

Last change with swapping height/length when rotation says so:

PyObject *magic_get_width(ILIObject *self, void *closure) {
    if (self->rotation == 0 || self->rotation == 180)
        return Py_BuildValue("i", self->width);
    else
        return Py_BuildValue("i", self->height);
}

PyObject *magic_get_height(ILIObject *self, void *closure) {
    if (self->rotation == 0 || self->rotation == 180)
        return Py_BuildValue("i", self->height);
    else
        return Py_BuildValue("i", self->width);
}

This takes care of rotation.

Interfaces

Ok, small detour made and back to interfaces. We will merge spi.c and gpio.c into one file. Or better we will keep files but create an interface file that will call proper functions from spi.c or gpio.c.
This requires refactoring, creating more headers and finally, function interface.c:

#include <stdint.h>
#include "ili.h"
#include "interface.h"
#include "spi.h"
#include "gpio.h"

void cmd(ILIObject *self, uint16_t data) {
    if (self->SPI == -1) {
        gpio_cmd(self, data);
    } else {
        spi_cmd(self, data);
    }
}

void data(ILIObject *self, uint16_t data) {
    if (self->SPI == -1) {
        gpio_data(self, data);
    } else {
        spi_data(self, data);
    }
}

void set_cs(ILIObject *self, int state) {
    if (self->SPI == -1) {
        gpio_set_cs(self, state);
    } else {
        spi_set_cs(self, state);
    }
}
void set_rst(ILIObject *self, int state) {
    if (self->SPI == -1) {
        gpio_set_rst(self, state);
    } else {
        spi_set_rst(self, state);
    }
}

To compile program we need to change setup.py:

from distutils.core import setup, Extension

setup(
    name="ili9325",
    ext_modules=[
        Extension("cili9325", ["_cili9325.c", "_cili.c", "ili.c", "cili9325.c", "interface.c", "spi.c", "gpio.c"],  extra_link_args=['-lwiringPi']),
        Extension("cili9486", ["_cili9486.c", "_cili.c", "ili.c", "cili9486.c", "interface.c", "spi.c", "gpio.c"],  extra_link_args=['-lwiringPi']),
    ],
)


Uff nice, we have one entry point for both SPI and GPIO. In theory, we can plug ILI9326 via SPI or ILI9486 via GPIO.

Colours

Almost missed it! On ILI9486 I have a problem with colours. This reminded me of the same problem in GfxLCD, so I found a solution there. We need to send data in two parts not one:

unsigned char buffer[2];

static void spi_send(ILIObject *self, uint16_t data) {
    buffer[0] = data >> 8;
    buffer[1] = data;
    result = wiringPiSPIDataRW(self->SPI, buffer, 2);   
}

Funny, same error made twice in different languages 🙂

Summary

We achieved our goal, we have working ILI9486 via SPI interface. But we have one more problem to solve, slow SPI! I’m not sure if this is an RPi, driver or implementation problem. But this is a huge disappointment for me.

I wonder if this is the fault of wiringPi? Or SPI is really so slow. Something to check but first, we have drawing_line to fix and other drawing functions to implement.

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 )

Google+ photo

You are commenting using your Google+ 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 )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.