PSoC 6 + Segger EmWin + MCUFriend 2.4″ TFT – Part 2

Summary

In the previous Article (Part 1), I used an Arduino and two open source libraries to figure out the startup configuration sequence for a low cost 2.4″ TFT from MCUFriend.  In Part 2, I will show you how to use that information to make a driver for a PSoC 6 running the Segger emWin graphics library.

The steps that I will follow are:

  1. Create an Example project for the CY8CKIT-028-TFT from CE223726 and test (make sure the emWin library works)
  2. Copy the Example and Update Schematic and Pin Out for the MCUFriend Shield.
  3. Modify the project and initialization code for the ILI9341
  4. Test

Create an Example Project from CE223726 and Test

Cypress has delivered a code example for the CY8CKIT-028-TFT shield called CE223726.  This CE has all of the hardware connection setup for that shield, plus the integrated emWin middleware and a simple main that just displays 9 different screens.

The shield has a Newhaven 2.4″ 320×240 TFT with a Sitronix ST7789 driver.  This display uses the “8080” interface for the display, the same parallel interface as my MCUFriend shield.

To run the code example, first create a new project from the File->Code Example menu.

When you filter the list to “tft” by typing in the “Filter by” box you will see CE223726…. select it, then press “Create Project”.   If it is not on your computer you need to press the little world symbol to download it.

Accept the defaults for the Target IDE by pressing Next.

Give the project a sensible name, or accept the default name.

Once you have the project.  Program it to make sure that it works.

Copy the Example and Update Schematic and Pin Out

Now that I have the code example project tested, I will make a copy of it to serve as a base for the mcufriend version.  In the workspace explorer, select the project then Copy it with ctrl-c.  Next, click on the workspace and Paste with ctrl-v.  Then, rename the project to “mcufriend”.  Here is the workspace explorer after the copy/paste/rename.

The schematic for this project is interesting.  These LCDs use what is called the 8080 interface.  It was named 8080 because it is the same 8-bit interface that the old 8080 CPUs used.  Instead of using a “bit-banged” interface (like the Arduino implementation) Cypress created a digital component that knows how to write to the screen.  Actually this is bad-ass.  You can see that the component has D0-D7, Data/Command (D/C), Chip Select, Write (nwr) and Read (NRD).  Inside of the block there is a FIFO and a timing circuit that will let you write 8-bits at a time to the screen.

Now on with modifying the project.  First, you notice that the 2.4″ TFT shield has a different pin out than the CY8CKIT-028-TFT.  Here is a picture of the back of the shield:

To sort this out I created this pin map.  The other thing to notice is that the pin called “LCD_RS” is really the “LCD_C/D” pin.  I am not sure why they called it “RS” (well my friend Rajesh figured it out… RS means Register Select).  Anyway, here is a picture of the spreadsheet.  Another interesting thing to notice is that Cypress choose not to include the Chip Select on the pinout of the shield and in fact it is always selected.

All right, you can see that the pins are different, so, the first step in fixing this project is to remap the pins to match the shield.  Open up the Design Wide Resource pin configuration screen from the Workspace Explorer.  And assign the Shield Pins to the correct PSoC 6 pins.  Notice that I added a UART (which you can ignore)

The next thing that I need to do is update the schematic to reflect the ILI9341 speed.  On page 226 of the ILI9341 datasheet you can see that during a write cycle the write pulse needs to be low twrl=15ns and high twrh=15ns and the whole cycle needs to be at least 66ns.  For the read cycle the read pulse low trdl=45ns and the trdh=90ns.  Unfortunately, I dont know what an “FM” read is versus a “ID” read… so I am going to assume we only do “ID” reads.

The input clock to the LCD Interface component sets the width of the output write pulse.  Specifically, the write output is always low for 1 clock cycle and high for 1 clock cycle.  For instance, if your clock is set to 10MHz (also known as a period 100ns) then your write output pulse will be 100ns (low) + 100ns(high).  For the read pulse you are given the ability to set the number of clocks for low and high.  In the above case of a 10MHz input clock if you set the low to 3 and the high to 5 you would end up with 3*100ns  (low) + 5*100ns (high) = 800ns total.

Armed with all of that information, we need to pick 3 numbers.  An input clock, the # of read low pulse and the # of read high pulses.  The write cycle needs to be at least 66ns so we need a minimum clock frequency of 30MHz which will have a period of 33ns.  For the read we need 45ns low and 90ns high and a total of 160ns.  This means the whole read cycle needs to be 160ns/33ns=4.8 clock cycles.  To achieve this ill select 2 low (for 66ns) and 3 high (99 ns) total 165ns.

Open the schematic, then double click the clock and change its frequency to 30MHz.  (notice that I renamed it to LCD_Clock)

Double click the GraphicLCDIntf to update the read transaction low pulse to have 2-clocks and the read transaction high pulse width to be 3-clocks.

But wait.  Why is the pulse width 40ns and 80ns?  That means that the input clock is set to 25MHz (40ns period).  Well it turns out that is exactly right.  But if we typed in 30MHz how did we end up with 25MHz?  If you look on the clocks tab of the design wide resources you will find that the source of the LCD_Clock is the Clk_Peri and it is running at 50MHz.  When PSoC Creator figures out a clock, it can only choose a whole number divider, also known a 2 to synthesize the LCD_Clk, which means that the output frequency will actually be 25MHz.  I am pretty sure that I could move things around and figure out a combination of dividers and clock frequencies to make it work, but that isnt the point today so Ill just move forward.

Modify the Initialization Code

With the schematic and pin out modified, we will turn our attention to the code.  There are several changes that need to be made to LCDConf.c

  1. Create a function called “_InitController_9341” to startup the screen (based on the learning from Part 1)
  2. Change the name of function “_InitController” to be “_InitController_st7789”
  3. Update the LCD_X_Config function with the correct orientation and color format
  4. Update the function “LCD_X_DisplayDriver” to call the correct Initialization function

In order to get the screen going, you need to send the commands/data that we discovered in Part 1.  The author of that code had a structure which I like for holding that data.  Specifically it is an array of uint8_ts with Command, Length of Data , Data 0-N and on and on.  This is exactly the format that I spit out from the Arduino code in the Part 1.  Here is the screenshot:

To use this data I create an array (called ILI9341_regValues_2_4 … the same name from the original library.

static const uint8_t ILI9341_regValues_2_4[]  = {        // BOE 2.4"                                                                                                                
0x1,0x0,                                  // Software Reset
0x28,0x0,                                 // Display Off
0x3A,0x1,0x55,                            // Pixel Format RGB=16-bits/pixel MCU=16-bits/Pixel
0xF6,0x3,0x1,0x1,0x0,                     // Interface control .. I have no idea
#if 0    
0xCF,0x3,0x0,0x81,0x30,                   // Not defined
0xED,0x4,0x64,0x3,0x12,0x81,              // Not defined
0xE8,0x3,0x85,0x10,0x78,                  // Not defined
0xCB,0x5,0x39,0x2C,0x0,0x34,0x2,          // Not defined
0xF7,0x1,0x20,                            // Not defined
0xEA,0x2,0x0,0x0,                         // Not defined  
    #endif
0xB0,0x1,0x0,                             // RGB Interface Control
0xB1,0x2,0x0,0x1B,                        // Frame Rate Control
0xB4,0x1,0x0,                             // Display Inversion Control
0xC0,0x1,0x21,                            // Power Control 1
0xC1,0x1,0x11,                            // Power Control 2
0xC5,0x2,0x3F,0x3C,                       // VCOM Control 1
0xC7,0x1,0xB5,                            // VCOM Control 2
0x36,0x1,0x48,                            // Memory Access Control

#if 0
0xF2,0x1,0x0,                             // Not defined
#endif

0x26,0x1,0x1,                             // Gamma Set
0xE0,0xF,0xF,0x26,0x24,0xB,0xE,0x9,0x54,0xA8,0x46,0xC,0x17,0x9,0xF,0x7,0x0,    // Positive Gamma Correction
0xE1,0xF,0x0,0x19,0x1B,0x4,0x10,0x7,0x2A,0x47,0x39,0x3,0x6,0x6,0x30,0x38,0xF,  // Negative Gamme Correction
0x11,0x0,                                 // Sleep Out
0x29,0x0,                                 // Display On
0x36,0x1,0x48,                            // Memory Access Control
0x2A,0x4,0x0,0x0,0x0,0xEF,                // Column Address Set = 239
0x2B,0x4,0x0,0x0,0x1,0x3F,                // Row Address Set  = 319
0x33,0x6,0x0,0x0,0x1,0x40,0x0,0x0,        // Vertical Scrolling Definition
0x37,0x2,0x0,0x0,                         // Vertical Scrolling Start Address
0x13,0x0,                                 // Normal Display ON
0x20,0x0,                                 // Display Inversion OFF
};


Notice that I have ifdef’d out the values that dont do anything.  In order to use the array, I create a function called _InitController_9341.  It

  1. Starts the component
  2. Sends a reset
  3. Then loops through the datastrcture from above sending Write Commands (GraphicLCDIntf_1_Write8_A0) and Write Data (GraphicLCDIntf_1_Write8_A1)
static void _InitController_9341(void)
{
    /* Start the parallel interface */
    GraphicLCDIntf_1_Start();

    /* Reset - High, Low (reset), High */
    Cy_GPIO_Set(LCD_RESET_N_0_PORT, LCD_RESET_N_0_NUM);
    GUI_Delay(20);
    Cy_GPIO_Clr(LCD_RESET_N_0_PORT, LCD_RESET_N_0_NUM);
    GUI_Delay(100);
    Cy_GPIO_Set(LCD_RESET_N_0_PORT, LCD_RESET_N_0_NUM);
    GUI_Delay(100);
    
    for(unsigned int i=0;i<sizeof(ILI9341_regValues_2_4);i++)
    {
        GraphicLCDIntf_1_Write8_A0(ILI9341_regValues_2_4[i]);
        printf("Command %02X\r\n",ILI9341_regValues_2_4[i]);
        i=i+1;
        unsigned int count;
        count = ILI9341_regValues_2_4[i];
        for(unsigned int j=0;j<count;j++)
        {
            i=i+1;
            printf("Data %02X\r\n",ILI9341_regValues_2_4[i]);
            GraphicLCDIntf_1_Write8_A1(ILI9341_regValues_2_4[i]);
        }
        
    }

}

Now we move onto the configuration function.  There are several changes that need to be made to it.

  1. What driver chip you are using?
  2. What is the bus interface you are going to use?
  3. What is the format of the RGB Data?
  4. How do you want the screen setup?

The function call on line 319 configures the driver and the bus interface.

GUIDRV_FlexColor_SetFunc(pDevice, &PortAPI, GUIDRV_FLEXCOLOR_F66709, GUIDRV_FLEXCOLOR_M16C0B8);

The driver is specified from table 33.42 on page 1193 of the emWin manual.  Specifically, you tell it to use GUIDRV_FLEXCOLOR_F66709 which you can see support ILI9341 amongst others.

Then you need to tell what bus interface to use.  When we setup the display, we told it that we wanted 16-bit color by sending 0x3A, 0x55.  Here is a screen shot from the ILI9341 datasheet.

Given that, the last parameter of the driver call is the bus format.  So, set the bus width to 16 bits per pixel, 8-bit bus.

GUIDRV_FlexColor_SetFunc(pDevice, &PortAPI, GUIDRV_FLEXCOLOR_F66709, GUIDRV_FLEXCOLOR_M16C0B8);

Now you need to tell it what bits mean what color.  For this display 16-bit color is encoded at 5-bits of Red, 6-bits of Green and 5-bits of Blue.  On Page 65 of the ILI9341 datasheet you can see that we should send 8-bits of command, 5 bits of red, 6-bits of green, 5-bits of blue, then the next pixel.

To tell emWin that, you need to pick GUICC_M565 (see the screen shot below from the Segger emWin documentation)

But it turns out that doesnt work?  The blue and green are FLIPPED.  How is that possible?  As I tried to figure this out I googled a bunch of things… read on the Segger forums etc.  All of the normal things.  Finally I sent a note to some of my friends at Cypress that went like this:

“There are several possibilities

  1. There is a bug in emWin
  2. There is a bug in the ILI9341
  3. There is a bug in the emWin documentation
  4. There is a bug in the ILI9341 documentation
  5. There is a bug in my firmware
  6. There is a bug in my brain.
  7. There is a bug in my understanding of the documentation

Personally I bet on #7″

Well it turned out that I was right.  It was #7.  The Red-Green-Blue order is set by register 36h

If you recall I copied setup from the Arduino code in Part 1.  When I called the setup I wrote 0x48 into that register… that is also known as BGR = 1 or “1=BGR color filter panel”.  It turns out that the example of the byte writing order above is just that… and example.  How did I figure this out?  Simple answer, thank you to Oleksandr in Ukraine for sorting that out for me because I was going out of my mind last night.

Now, the other little nasty part of things is that if I had written BGR=0 it still would not have worked.  It turns out that emWin overwrites that bit when it rotates the screen.  Why?  Who the hell knows.  Anyway, here is what Oleksandr says, “In your code in the initialization sequence you set 0x36 register to 0x48 so, BRG mode must be active and GUICC_565 palette must be correct.  But FlexColor driver itself writes to the 0x36 register in order to setup display orientation. By default, driver set the BRG bit to zero, activating RGB mode.”  Here is a rather vague description of what happens from the emWin documentation. [There is still an error here]

With the color order sorted out, the last thing that I change is the orientation of the display on line 309.  Here is the entire configuration function:

void LCD_X_Config(void) {
    GUI_DEVICE * pDevice;
    CONFIG_FLEXCOLOR Config = {0};
    GUI_PORT_API PortAPI = {0};
    //
    // Set the display driver and color conversion
    //
    // GUICC_565
    // GUICC_M565
    pDevice = GUI_DEVICE_CreateAndLink(DISPLAY_DRIVER, GUICC_565, 0, 0);
    //
    // Display driver configuration
    //
    LCD_SetSizeEx    (0, XSIZE_PHYS,   YSIZE_PHYS);
    LCD_SetVSizeEx   (0, VXSIZE_PHYS,  VYSIZE_PHYS);
    //
    // Orientation
    //
    //Config.Orientation   = GUI_MIRROR_Y | GUI_SWAP_XY;
    Config.Orientation   = GUI_SWAP_XY;
    GUIDRV_FlexColor_Config(pDevice, &Config);
    //
    // Set controller and operation mode
    //
    PortAPI.pfWrite8_A0  = GraphicLCDIntf_1_Write8_A0;
    PortAPI.pfWrite8_A1  = GraphicLCDIntf_1_Write8_A1;
    PortAPI.pfWriteM8_A1 = GraphicLCDIntf_1_WriteM8_A1;
    PortAPI.pfRead8_A1  = GraphicLCDIntf_1_Read8_A1;
    PortAPI.pfReadM8_A1  = GraphicLCDIntf_1_ReadM8_A1;
    GUIDRV_FlexColor_SetFunc(pDevice, &PortAPI, GUIDRV_FLEXCOLOR_F66709, GUIDRV_FLEXCOLOR_M16C0B8);
}

In the LCD_X_DisplayDriver function I will simply call the correct initialization function… the one we just created … called _InitController_9341

int LCD_X_DisplayDriver(unsigned LayerIndex, unsigned Cmd, void * pData) {
    int r;

    GUI_USE_PARA(LayerIndex);
    GUI_USE_PARA(pData);

    switch (Cmd) {
        case LCD_X_INITCONTROLLER: {
            //
            // Called during the initialization process in order to set up the
            // display controller and put it into operation. If the display
            // controller is not initialized by any external routine, this needs
            // to be adapted by the customer...
            //
            // ...
            _InitController_9341();
            return 0;
        }
        default:
            r = -1;
    }
    return r;
}

Test

Finally a program and test… and lookey here… it works:

You can find all of these projects on my github site: https://github.com/iotexpert/MCUFriend or clone it with git clone git@github.com:iotexpert/MCUFriend.git

Embedded Graphics Index

Embedded Graphics
Embedded Graphics Overview
TFT Displays & Drivers
TFT Controllers
PSoC 6 + Segger EmWin + MCUFriend 2.4" TFT - Part 1
PSoC 6 + Segger EmWin + MCUFriend 2.4" TFT - Part 2
MCU Friend 3.5" Identification

PSoC 6 + Segger EmWin + MCUFriend 2.4″ TFT – Part 1

Summary

This article takes you through the steps that I went through to figure out the startup sequence for a 2.4″ TFT MCUFriend display (Part 1), and then port it to a PSoC 6 running Segger emWin graphics library (Part 2)

I recently tried out the Cypress CY8CKIT-028-TFT with the PSoC Creator Example Project, CE223726.  This code example builds a project using the Segger EmWin library, its pretty cool.  And, I have been interested in LCDs so I purchased some random LCD shields from eBay, including a rather generic looking 2.4″ TFT from bang good (a generic Chinese reseller).  The entirety of the documentation is silkscreen plus the sticker you see in the picture, which just tells you that it has an ILI9341 LCD driver.

  

This left me with a number of questions  Starting with, How do you initialize the screen?  Obviously you can find the Ilitech ILI9341 datasheet, but then what?  The datasheet has fifty billion parameters, and not much advice about what to do.  Moreover, after googling around, I discovered that there is an absolute rogues gallery of bad advice about these screens.  Horrible horrible.  Finally, I found an Arduino library called “MCUFRIEND_kbv” that seemed like it was coherent.  So, I installed it and got the screen working – just to prove that it worked.  But I’m still not going to use an Arduino, so I need to port the initialization to my Segger emWin project.

Here is what I did to sort this out:

  1. Install the Arduino libraries to use the 2.4″ TFT MCUFriend Shield
  2. Fix & Run the Arduino Example project to prove that it works
  3. Modify the MCUFRIEND_kbv.c to dump the ILI9341 Startup sequence
  4. Verify the startup sequence against the ILI9341 data sheet (and fix the errors)
  5. Modify the CE223726 to use the ILI9341 shield

Install the Arduino Libraries

In order to use the screen I need the Arduino libraries that drive it.  Start by installing the Adafruit GFX library select Sketch–>Include Library–>Manage Libraries…

In the filter box type “gfx”.  Once you find the Adafruit GFX Library, you can click Install.

Then you need to install mcufriend_kbv library.  Type “mcuf” into the filter.  Then select install.

These two operations will add the directories to your library:

Fix & Run the Arduino Example project

After you have added the Libraries, you can then make a project based on one of the MCUFriend existing examples.  For this test case Ill pick File–>Examples–>MCUFRIEND_kbv–>diagnose_TFT_support

For this project to run you need to include the AdafruitGFX library. To do that select Sketch–>Include Library–>Adafruit GFX Library

And also the SPI library with Sketch–>Include Library–>SPI

Once that is done you can click on the build checkmark, then the download button and your screen should look something like this:

And you should have this:

And if you start the serial port monitor you will get this:

OK.  This is good.  This means that the screen is functioning and the Arduino library properly identifies it as a ILI9341.

Modify the MCUFRIEND_kbv.cpp

In order to get one of these screens going you need to set a bunch of parameters before you can attach it to your Graphics library.  But, what is the sequence.  I first started looking through the code…. but honestly it is a mess.  Here is a little snip of it:

case 0x9341:
common_9341:
_lcd_capable = AUTO_READINC | MIPI_DCS_REV1 | MV_AXIS | READ_24BITS;
static const uint8_t ILI9341_regValues_2_4[] PROGMEM = {        // BOE 2.4"                                                                                                                
0xF6, 3, 0x01, 0x01, 0x00,  //Interface Control needs EXTC=1 MV_EOR=0, TM=0, RIM=0                                                                                                     
0xCF, 3, 0x00, 0x81, 0x30,  //Power Control B [00 81 30]                                                                                                                               
0xED, 4, 0x64, 0x03, 0x12, 0x81,    //Power On Seq [55 01 23 01]                                                                                                                       
0xE8, 3, 0x85, 0x10, 0x78,  //Driver Timing A [04 11 7A]                                                                                                                               
0xCB, 5, 0x39, 0x2C, 0x00, 0x34, 0x02,      //Power Control A [39 2C 00 34 02]                                                                                                         
0xF7, 1, 0x20,      //Pump Ratio [10]                                                                                                                                                  
0xEA, 2, 0x00, 0x00,        //Driver Timing B [66 00]                                                                                                                                  
0xB0, 1, 0x00,      //RGB Signal [00]                                                                                                                                                  
0xB1, 2, 0x00, 0x1B,        //Frame Control [00 1B]                                                                                                                                    
//            0xB6, 2, 0x0A, 0xA2, 0x27, //Display Function [0A 82 27 XX]    .kbv SS=1                                                                                                 
0xB4, 1, 0x00,      //Inversion Control [02] .kbv NLA=1, NLB=1, NLC=1                                                                                                                  
0xC0, 1, 0x21,      //Power Control 1 [26]                                                                                                                                             
0xC1, 1, 0x11,      //Power Control 2 [00]                                                                                                                                             
0xC5, 2, 0x3F, 0x3C,        //VCOM 1 [31 3C]                                                                                                                                           
0xC7, 1, 0xB5,      //VCOM 2 [C0]                                                                                                                                                      
0x36, 1, 0x48,      //Memory Access [00]                                                                                                                                               
0xF2, 1, 0x00,      //Enable 3G [02]                                                                                                                                                   
0x26, 1, 0x01,      //Gamma Set [01]                                                                                                                                                   
0xE0, 15, 0x0f, 0x26, 0x24, 0x0b, 0x0e, 0x09, 0x54, 0xa8, 0x46, 0x0c, 0x17, 0x09, 0x0f, 0x07, 0x00,
0xE1, 15, 0x00, 0x19, 0x1b, 0x04, 0x10, 0x07, 0x2a, 0x47, 0x39, 0x03, 0x06, 0x06, 0x30, 0x38, 0x0f,
};

Rather than try to figure out all of the stuff that I “THINK” that it sends, I decided to modify the WriteCmdParamN method to just print out what it actually sends.  And I decided to print it out in a format that would be easy to import into my program:  You can see that each time a command is sent, I print out the command number, followed by the number of bytes, followed by the actual bytes (this is similar to the original code).

#define ARH_DEBUG
void MCUFRIEND_kbv::WriteCmdData(uint16_t cmd, uint16_t dat) { writecmddata(cmd, dat); }
static void WriteCmdParamN(uint16_t cmd, int8_t N, uint8_t * block)
{
#ifdef ARH_DEBUG
Serial.print(F("0x"));
Serial.print(cmd,HEX);
Serial.print(F(",0x"));
Serial.print(N,HEX);
Serial.print(F(","));
#endif
CS_ACTIVE;
WriteCmd(cmd);
while (N-- > 0) {
uint8_t u8 = *block++;
#ifdef ARH_DEBUG
Serial.print(F("0x"));
Serial.print(u8,HEX);
Serial.print(F(","));
#endif
write8(u8);
if (N && is8347) {
cmd++;
WriteCmd(cmd);
}
}
#ifdef ARH_DEBUG
Serial.println(F(" "));
#endif
CS_IDLE;
}

When I download and run the program you can see that it

  1. Issues a reset
  2. Then sends the commands/data to configure the screen

OK thats good.  Now, the bad part, when I look at the commands in the ILI9341 documentation it turns out that 0xCF, 0xED, 0xE8, 0xCB, 0xF8, 0xEA and 0xF2 are not actually commands.  Hmmm… I suppose that they don’t do any harm?  But they also don’t appear to actually do anything.  I guess beggars can’t be choosers, so I won’t be critical.

In the next Article I’ll show you how to take this startup code and put it into a PSoC with Segger emWin.

Embedded Graphics Index

Embedded Graphics
Embedded Graphics Overview
TFT Displays & Drivers
TFT Controllers
PSoC 6 + Segger EmWin + MCUFriend 2.4" TFT - Part 1
PSoC 6 + Segger EmWin + MCUFriend 2.4" TFT - Part 2
MCU Friend 3.5" Identification

RS Components & the Elkhorn Creek

Summary

Earlier this year I hosted RS Components at my house in Kentucky.  Here are three videos that they recorded about the Elkhorn Creek Water Level System.  Whoever edited them knew what they were doing.  Thanks to RS Components.

Part 1

Part 2

Part 3