ILI9325 in Python and C – performance

I have some crazy idea but before we need to do something with GfxLCD performance. Using ILI9325 with Python is a way to slow:

pi@raspberrypi2b:~/workspace/gfxlcd/gfxlcd/demos $ time python3 ili9325.py
real    0m14.528s
user    0m14.080s
sys     0m0.070s

Filling screen with size 240×320 in 14s is not impressive. Let’s see what happens when we move a layer down to C.

C

I’m not an expert in C so the code can scare you ๐Ÿ™‚
Let’s take Python code and create a demo in the simplest way. Our goal is to fill the screen with a colour.
I choose WiringPi as a library to control GPIOs.

I have a header file (ili9325.h) with pins, macros and functions declarations.

#define PIN_LED 6
#define PIN_CS 18
#define PIN_RS 27
#define PIN_W 17
#define PIN_DB8 22
#define PIN_DB9 23
#define PIN_DB10 24
#define PIN_DB11 5
#define PIN_DB12 12
#define PIN_DB13 16
#define PIN_DB14 20
#define PIN_DB15 21
#define PIN_RST 25
#define min(a,b) (((a)<(b))?(a):(b)) #define max(a,b) (((a)>(b))?(a):(b))
#define abs(x) ((x)>0?(x):-(x))

void set_pins(uint8_t data);
void set_pin(uint8_t pin, unsigned char value);
void cmd(uint16_t data);
void data(uint16_t data);
void send(uint16_t data);
void swap(uint16_t*, uint16_t*);
void set_area(uint16_t pos_x1, uint16_t pos_y1, uint16_t pos_x2, uint16_t pos_y2);
uint16_t get_color(uint16_t r, uint16_t g, uint16_t b);

void init_display();
void set_color(uint16_t r, uint16_t g, uint16_t b);
void set_backgroundcolor(uint16_t r, uint16_t g, uint16_t b);
void fill_rect(uint16_t pos_x1, uint16_t pos_y1, uint16_t pos_x2, uint16_t pos_y2);

Functions are grouped into two groups, internal and external. Why? External functions are meant to be used by the user.

Main code in ili9325.c file:

#include <wiringPi.h>
#include <stdint.h>
#include <stdio.h>
#include "ili9325.h"

uint16_t color = 0;
uint16_t background_color = 0;

uint16_t width = 240;
uint16_t height = 320;

void setup_pins() {
    wiringPiSetupGpio();
    pinMode (PIN_LED, OUTPUT);
    pinMode (PIN_RS, OUTPUT);
    pinMode (PIN_W, OUTPUT);
    pinMode (PIN_DB8, OUTPUT);
    pinMode (PIN_DB9, OUTPUT);
    pinMode (PIN_DB10, OUTPUT);
    pinMode (PIN_DB11, OUTPUT);
    pinMode (PIN_DB12, OUTPUT);
    pinMode (PIN_DB13, OUTPUT);
    pinMode (PIN_DB14, OUTPUT);
    pinMode (PIN_DB15, OUTPUT);
    pinMode (PIN_RST, OUTPUT);
    pinMode (PIN_CS, OUTPUT);

    digitalWrite (PIN_LED, LOW);
    digitalWrite (PIN_RS, LOW);
    digitalWrite (PIN_W, LOW);
    digitalWrite (PIN_DB8, LOW);
    digitalWrite (PIN_DB9, LOW);
    digitalWrite (PIN_DB10, LOW);
    digitalWrite (PIN_DB11, LOW);
    digitalWrite (PIN_DB12, LOW);
    digitalWrite (PIN_DB13, LOW);
    digitalWrite (PIN_DB14, LOW);
    digitalWrite (PIN_DB15, LOW);
    digitalWrite (PIN_RST, LOW);
    digitalWrite (PIN_CS, HIGH);

    digitalWrite(PIN_LED, HIGH);
}

int main (void) {
    setup_pins();
    init_display();

    delay(1);

    background_color = get_color(255,128,0);
    fill_rect(0,0,100,200);
    background_color = get_color(128,255,128);
    fill_rect(0,0,width-1,height-1);
    background_color = get_color(255,0,0);
    fill_rect(0,0,10,10);
    fill_rect(229,0,239,10);
    fill_rect(0,309,10,319);
    fill_rect(229,309,239,319);

    return 0;
}

void set_color(uint16_t r, uint16_t g, uint16_t b) {
    color = get_color(r,g,b);
}
void set_backgroundcolor(uint16_t r, uint16_t g, uint16_t b) {
    background_color = get_color(r,g,b);
}
uint16_t get_color(uint16_t r, uint16_t g, uint16_t b) {
    uint32_t rgb = (r << 16) | (g << 8) | b; return ((rgb & 0x00f80000) >> 8) | ((rgb & 0x0000fc00) >> 5) | ((rgb & 0x000000f8) >> 3);
}

void set_area(uint16_t pos_x1, uint16_t pos_y1, uint16_t pos_x2, uint16_t pos_y2) {
    cmd(0x0020); data(pos_x1);
    cmd(0x0021); data(pos_y1);
    cmd(0x0050); data(pos_x1);
    cmd(0x0052); data(pos_y1);
    cmd(0x0051); data(pos_x2);
    cmd(0x0053); data(pos_y2);
    cmd(0x0022);
}

void init_display(){

    digitalWrite (PIN_CS, HIGH);
    digitalWrite (PIN_RST, HIGH);
    delay(0.05);
    digitalWrite (PIN_RST, LOW);
    delay(0.05);
    digitalWrite (PIN_RST, HIGH);
    delay(0.05);

    cmd(0x0001);
    data(0x0100);

    // set 1 line inversion
    cmd(0x0002);
    data(0x0200);

    // set GRAM write direction and BGR=1
    cmd(0x0003);
    data(0x1030);

    // Resize register
    cmd(0x0004);
    data(0x0000);
    // set the back porch and front porch
    cmd(0x0008);
    data(0x0207);
    // set non-display area refresh cycle ISC[3:0]
    cmd(0x0009);
    data(0x0000);
    // FMARK function
    cmd(0x000A);
    data(0x0000);
    // RGB interface setting
    cmd(0x000C);
    data(0x0000);
    // Frame marker Position
    cmd(0x000D);
    data(0x0000);
    // RGB interface polarity
    cmd(0x000F);
    data(0x0000);

    // ************* Power On sequence ****************
    // SAP, BT[3:0], AP, DSTB, SLP, STB
    cmd(0x0010);
    data(0x0000);
    // DC1[2:0], DC0[2:0], VC[2:0]
    cmd(0x0011);
    data(0x0007);
    // VREG1OUT voltage
    cmd(0x0012);
    data(0x0000);
    // VDV[4:0] for VCOM amplitude
    cmd(0x0013);
    data(0x0000);
    cmd(0x0007);
    data(0x0001);
    delay(0.5);  // Dis-charge capacitor power voltage
    // SAP, BT[3:0], AP, DSTB, SLP, STB
    cmd(0x0010);
    data(0x1690);
    // Set DC1[2:0], DC0[2:0], VC[2:0]
    cmd(0x0011);
    data(0x0227);
    delay(0.5);
    cmd(0x0012);
    data(0x000D);
    delay(0.5);
    // VDV[4:0] for VCOM amplitude
    cmd(0x0013);
    data(0x1200);
    // 04  VCM[5:0] for VCOMH
    cmd(0x0029);
    data(0x000A);
    // Set Frame Rate
    cmd(0x002B);
    data(0x000D);
    delay(0.5);
    // GRAM horizontal Address
    cmd(0x0020);
    data(0x0000);
    // GRAM Vertical Address
    cmd(0x0021);
    data(0x0000);

    // ************* Adjust the Gamma Curve *************
    cmd(0x0030);
    data(0x0000);
    cmd(0x0031);
    data(0x0404);
    cmd(0x0032);
    data(0x0003);
    cmd(0x0035);
    data(0x0405);
    cmd(0x0036);
    data(0x0808);
    cmd(0x0037);
    data(0x0407);
    cmd(0x0038);
    data(0x0303);
    cmd(0x0039);
    data(0x0707);
    cmd(0x003C);
    data(0x0504);
    cmd(0x003D);
    data(0x0808);

    // ************* Set GRAM area *************
    // Horizontal GRAM Start Address
    cmd(0x0050);
    data(0x0000);
    // Horizontal GRAM End Address
    cmd(0x0051);
    data(0x00EF);
    // Vertical GRAM Start Address
    cmd(0x0052);
    data(0x0000);
    // Vertical GRAM Start Address
    cmd(0x0053);
    data(0x013F);
    // Gate Scan Line
    cmd(0x0060);
    data(0xa700);

    // NDL, VLE, REV
    cmd(0x0061);
    data(0x0001);
    // set scrolling line
    cmd(0x006A);
    data(0x0000);

    // ************* Partial Display Control *************
    cmd(0x0080);
    data(0x0000);
    cmd(0x0081);
    data(0x0000);
    cmd(0x0082);
    data(0x0000);
    cmd(0x0083);
    data(0x0000);
    cmd(0x0084);
    data(0x0000);
    cmd(0x0085);
    data(0x0000);

    // ************* Panel Control *************
    cmd(0x0090);
    data(0x0010);
    cmd(0x0092);
    data(0x0000);
    // 262K color and display ON
    cmd(0x0007);
    data(0x0133);
    delay(0.005);
}

void send(uint16_t data) {
    digitalWrite(PIN_CS, LOW);
    set_pins(data >> 8);
    digitalWrite(PIN_W, LOW);
    digitalWrite(PIN_W, HIGH);
    set_pins(data);

    digitalWrite(PIN_W, LOW);
    digitalWrite(PIN_W, HIGH);
    digitalWrite(PIN_CS, HIGH);
}

void set_pins(uint8_t data) {
    set_pin(PIN_DB8, (data >> 0));
    set_pin(PIN_DB9, (data >> 1));
    set_pin(PIN_DB10, (data >> 2));
    set_pin(PIN_DB11, (data >> 3));
    set_pin(PIN_DB12, (data >> 4));
    set_pin(PIN_DB13, (data >> 5));
    set_pin(PIN_DB14, (data >> 6));
    set_pin(PIN_DB15, (data >> 7));
}

void set_pin(uint8_t pin, unsigned char value) {
    if (value & 0x01) { digitalWrite(pin, HIGH);} else {digitalWrite(pin, LOW);}
}

void cmd(uint16_t data) {
    digitalWrite(PIN_RS, LOW); send(data);
}

void data(uint16_t data) {
    digitalWrite(PIN_RS, HIGH); send(data);
}

void swap(uint16_t *a, uint16_t *b) {
   uint16_t t;
   t  = *b; *b = *a; *a = t;
}

void fill_rect(uint16_t pos_x1, uint16_t pos_y1, uint16_t pos_x2, uint16_t pos_y2) {
    int size = (abs(pos_x2 - pos_x1) + 1) * (abs(pos_y2 - pos_y1) + 1);
    set_area(min(pos_x1, pos_x2), min(pos_y1, pos_y2), max(pos_x1, pos_x2), max(pos_y1, pos_y2));
    int i;
    for(i=0; i<size; i++){
        data(background_color);
    }
}

After some time I got results and one bug. I can’t fill the whole screen as the first operation. It only fills some part of it. So first I call some dummy fill and next I call a full-size fill.ย  Then it works. No idea why ๐Ÿ™‚
My bet is with some delay missing somewhere but no idea where.
How does it perform?

pi@raspberrypi2b:~/workspace/perf $ gcc -Wall -o ili9325 ili9325.c -lwiringPi
pi@raspberrypi2b:~/workspace/perf $ time ./ili9325

real    0m0.262s
user    0m0.240s
sys     0m0.010s

This is a HUGE difference! 14s Vs 0.3s. This is a good direction but I see C not as friendly as Python so what now?
Maybe we can create Python bindings to C code and see a minimal performance loss?

Python bindings

Now something new for me, learn how to wrap C code into Python. After a few tutorials, itย seemsย that it is not too hard but requires some code.

Quick summarize what needs to be done:
1. Create a new file which will hold all C to Python code. We can do all in one file but with this approach, we can compile and use C program and have bindings.

2. Define docstrings for all functions that we will export and for the module.

3. Define an array of functions that we will export, with name, docblock and proxy function.

4. Define a function that initializes a module.

5. Write proxy functions, they usually convert args from Python to C, call proper C function and return result converted to Python.

6. Write a setup.py that will compile extension

Ok. We can start, create file _ili9325.c and start with headers:

#include <Python.h>
#include "ili9325.h"

The first line brings lots of magic that you will see later ๐Ÿ™‚

Quick thinking about what we need to export to Python. Our goal is to fill a screen. So we need to initialize, set colour/background and fill. Those are four functions so four docblocks + one for module:

static char module_docstring[] =
    "ILI9325 in C";
static char init_display_docstring[] =
    "init_display";
static char fill_rect_docstring[] =
    "fill_rect";
static char set_color_docstring[] =
    "set_color";
static char set_backgroundcolor_docstring[] =
    "set_backgroundcolor";

As you can see I’m not using any fancy description:D
We have docs so it would be nice to have functions:

static PyObject *ili9325_init_display(PyObject *self);
static PyObject *ili9325_fill_rect(PyObject *self, PyObject *args);
static PyObject *ili9325_set_color(PyObject *self, PyObject *args);
static PyObject *ili9325_set_backgroundcolor(PyObject *self, PyObject *args);

static PyMethodDef module_methods[] = {
    {"init_display", ili9325_init_display, METH_VARARGS, init_display_docstring},
    {"fill_rect", ili9325_fill_rect, METH_VARARGS, fill_rect_docstring},
    {"set_color", ili9325_set_color, METH_VARARGS, set_color_docstring},
    {"set_backgroundcolor", ili9325_set_backgroundcolor, METH_VARARGS, set_backgroundcolor_docstring},
    {NULL, NULL, 0, NULL}
};

What is going on there? First, we declare functions that will work as a proxy between Python call and C function. We need it to convert arguments and set proper typing. Do not forget that C is strongly typed.
We also convert return from C to Python.

Next, we declare an array that joins all functions, names and docs into something usable.

Time for module’s init:

PyMODINIT_FUNC PyInit_ili9325(void)
{
    PyObject *module;
    static struct PyModuleDef moduledef = {
        PyModuleDef_HEAD_INIT,
        "ili9325",
        module_docstring,
        -1,
        module_methods,
        NULL,
        NULL,
        NULL,
        NULL
    };
    module = PyModule_Create(&moduledef);
    if (!module) return NULL;

    return module;
}

Our module has a name ili9325.

Next, we can create proxy functions:

static PyObject *ili9325_init_display(PyObject *self) {
    init_display();
    return Py_None;
}


static PyObject *ili9325_fill_rect(PyObject *self, PyObject *args) {
    int pos_x1, pos_x2, pos_y1, pos_y2;
    if (!PyArg_ParseTuple(args, "IIII", &pos_x1, &pos_y1, &pos_x2, &pos_y2)) {
        return NULL;
    }
    fill_rect(pos_x1, pos_y1, pos_x2, pos_y2);

    return Py_None;
}

static PyObject *ili9325_set_color(PyObject *self, PyObject *args) {
    int r, g, b;
    if (!PyArg_ParseTuple(args, "III", &r, &g, &b)) {
        return NULL;
    }
    set_color(r,g,b);

    return Py_None;
}

static PyObject *ili9325_set_backgroundcolor(PyObject *self, PyObject *args) {
   int r, g, b;
    if (!PyArg_ParseTuple(args, "III", &r, &g, &b)) {
        return NULL;
    }
    set_backgroundcolor(r,g,b);

    return Py_None;
}

See that our functions always accept a PyObject type. To get desired values from it we use PyArg_ParseTuple, I is a unsigned int type.

Finally, we must write a file that will tell python how to compile our extension:

from distutils.core import setup, Extension

setup(
    name="ili9325",
    ext_modules=[Extension("ili9325", ["_ili9325.c", "ili9325.c"],  extra_link_args=['-lwiringPi'])],
)

We are using extra_link_args to link wiringPi library with our code.

To start compiling process we need to call:

python3 setup.py build_ext --inplace

Got some warnings but it compiled to ili9325.cpython-34m.so file. Not a friendly name ๐Ÿ™‚ Renamed to ili9325.so.

Time for test, Python code that uses this lib:

import ili9325

ili9325.init_display()
ili9325.set_backgroundcolor(255,128,0)
ili9325.fill_rect(0,0,100,200)

And fail… LCD initialized but nothing on screen… what the? Maybe this is the same bug that in C? Let’s check the bigger test:

import ili9325

print(dir(ili9325))
print("init")
ili9325.init_display()
ili9325.set_backgroundcolor(255,128,0)
ili9325.fill_rect(0,0,100,200)
ili9325.set_backgroundcolor(128,255,128)
ili9325.fill_rect(0,0,240-1,320-1)
ili9325.set_backgroundcolor(255,0,0)
ili9325.fill_rect(0,0,10,10)
ili9325.fill_rect(229,0,239,10)
ili9325.fill_rect(0,309,10,319)
ili9325.fill_rect(229,309,239,319)

And there is something! Funny, witch each run I have different result ๐Ÿ™‚ Not good. But works.
We can check performance:

pi@raspberrypi2b:~/workspace/perf $ time python3 test.py
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'fill_rect', 'init_display', 'set_backgroundcolor', 'set_color']
init

real    0m0.426s
user    0m0.420s
sys     0m0.010s
pi@raspberrypi2b:~/workspace/perf $

Good! This is acceptable ๐Ÿ™‚

Before we plan our next steps we need to find what caused the bug. It must be something with delay somewhere but where…

Gosh! It is so simple… Someone who knows C should spot it right away. In Python delay works with second as a base but in C it is a millisecond. Delays are wrong ๐Ÿ™‚
After the fix, display works as it should.

Summary

Another step in endless struggle to display everything everywhere ๐Ÿ™‚
We have a knowledge how to wrap C in Python and most importantly we know that C offers good performance. So, next step is to write a driver for GfxLcd library that goes down to C level.
And we need to check SPI and ILI9486 ๐Ÿ™‚

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 )

w

Connecting to %s