Cypress & MBED OS

Summary

This morning I saw something exciting.  When I went to the ARM mbed-os GitHub site I saw the latest accepted merge commit was from vmedcy and it says something interesting.  “Add Cypress PSoC 6 Targets”.  I cannot even begin to describe how gratifying it is to see this commit.

But what does it mean?  Lets look.  I am using a Mac, so I simply install the MBED-CLI using the instructions found on the MBED CLI webpage.

Like all good embedded people, the first thing to do is make sure that the toolchain and everything else works using a simple blinking LED project.  And, the easiest way to do this is to import the ARM example using: “mbed import mbed-os-example-blinky”.

The current release of mbed-os is 5.11.3 which you can see on the Tags page of the mbed-os GitHub site.  Notice in the screenshot above that the commit number for mbed-os is “a8d1d2…”  which means that by default, when you import a project it gives you the “latest” version of mbed-os, which really means the latest official release.

But, I want to use the version that was accepted with the new Cypress stuff (which will become part of the official release starting with mbed-os-5.11.4).  I can do this by running “cd mbed-os” and “mbed update master”.  This will move the git repository to point at the head of the master branch, AKA the latest and greatest.  When I do that I get a version that looks like “32525…”

And when I look at the commit history in GitHub I can see the interesting commit has the same number.

Now to the good stuff.  When I look in the targets directory I see that there is a TARGET_CYPRESS.  Where is see two targets:

  • TARGET_PSOC – the stuff that Cypress created
  • TARGET_PSOC6_FUTURE – a really cool target that Future Electronics created.

When when I look in the Cypress directory I see a bunch of my favorite Cypress PSoC 6 and WICED wireless development Kits.

  • TARGET_CY8CKIT_062_BLE
  • TARGET_CY8CKIT_062_4343W
  • TARGET_CY8CKIT_062_WIFI_BT
  • TARGET_CYW943012P6EVB_01
  • TARGET_CY8CMOD_062_4343W
  • TARGET_CY8CPROTO_062_4343W

I happen to have a CY8CPROTO_062_4343W on my desk at home.  This kit has a bunch of cool stuff on it, but most importantly it a 4343W WiFi Bluetooth Combo chip and a PSoC 6 together.  Finally, A PSoC 6 2M (which is really called CY8C624ABZI-D44) and a WICED 4343W – a Dual Band 802.11n and Bluetooth 5.0 Combo.  Here is a picture from the development kit guide.

Now we have something to program.  Let’s look at the blinking LED example program.  It resides in the file “main.cpp” which is in the root directory of the “mbed-os-example-blinky”

/* mbed Microcontroller Library
 * Copyright (c) 2018 ARM Limited
 * SPDX-License-Identifier: Apache-2.0
 */

#include "mbed.h"
#include "stats_report.h"

DigitalOut led1(LED1);

#define SLEEP_TIME                  500 // (msec)
#define PRINT_AFTER_N_LOOPS         20

// main() runs in its own thread in the OS
int main()
{
    SystemReport sys_state( SLEEP_TIME * PRINT_AFTER_N_LOOPS /* Loop delay time in ms */);

    int count = 0;
    while (true) {
        // Blink LED and wait 0.5 seconds
        led1 = !led1;
        wait_ms(SLEEP_TIME);

        if ((0 == count) || (PRINT_AFTER_N_LOOPS == count)) {
            // Following the main thread wait, report on the current system status
            sys_state.report_state();
            count = 0;
        }
        ++count;
    }
}

This looks simple enough, but I am never a fan of “other” stuff in the blinking LED example.  So Ill trim it down to the most basic.

/* mbed Microcontroller Library
 * Copyright (c) 2018 ARM Limited
 * SPDX-License-Identifier: Apache-2.0
 */

#include "mbed.h"

DigitalOut led1(LED1);

#define SLEEP_TIME                  500 // (msec)

// main() runs in its own thread in the OS
int main()
{
    while (true) {
        // Blink LED and wait 0.5 seconds
        led1 = !led1;
        wait_ms(SLEEP_TIME);
    }
}

All-right, how do I compile this?  Well, run the mbed command line interface with “mbed compile -m CY8CPROTO_062_4343W -t GCC_ARM“… and after a bit of compiling you should end up with a window like this:

 

Now I have a “hex file”.  How do I program it?  There are three ways.

#1 You can add the “-f” option to the command line and it will “flash” the device after it gets done compiling using PyOCD.  In order to do this your development kit’s KitProg 3 must be in the DAPLink mode.  To get into this mode hold down the KitProg button for 2 seconds and the LED will turn off. (If the LED turns on that means you put the programmer into CMSIS-DAP mode, so hold down the button for 2 seconds again).  At the time of this writing the -f option doesn’t work in the released version of mbed-cli,  but that will be fixed shortly with an update to the program debug system in mbed (hopefully by the time you read this)

#2 Copy the hex file from the BUILD directory onto the Mass Storage device called “DAP Link” using the finder. (drag and drop).  To use this method your KitProg needs to be in DAPLink mode.  (so follow the steps above)

#3 Use the “Cypress Programmer” to program the flash.  You can download it for Windows, Mac or Linux from this link on cypress.com  When I run Cypress Programmer it looks like this:

Open the hex file by pressing the “Open” button and navigating into the BUILD directory to find the hex file called “mbed-os-example-blinky.hex”

Then press “connect” and “program”

Nothing happens on my development kit?  So I press the reset button and now I get a blinking LED.  But why do I have to press reset?   Do you see the “Reset Chip” checkbox at the top of the “Program Settings” window?  That has to be clicked.  Now when you program, the debugger will reset the chip and you will be off to the races.

So, what was going on with the rest of that program that came by default?  If you attach a serial terminal to the devkit you will see that the blinky example program is putting out information about the RTOS.

It turns out the mbed-os is really an os… an operation system.  Actually it is a real time operating system that traces it genealogy to RTX, a product from the old Keil Corporation which was acquired by ARM…. a lot more on this later.

One final note that may cause confusion (I certainly have been confused).  There are four different modes of KitProg 3 (the programmer that is built into most of the Cypress development kits).

Mode LED Change modes
CMSIS-DAP BULK Solid LED Short button press toggles between BULK and HID
CMSIS-DAP HID Breathing LED Short button press toggles between BULK and HID
DAPLink LED Off Hold KitProg button for >2 seconds (gets in and out of this mode)
B00tloader Fast blinking LED hold reset button and plug in kit

In order to program from the command line using “mbed compile -f” you need to be in “DAPLink” mode.  In order to program with Cypress Programmer you need one of the CMSIS-DAP modes.

 

 

PSoC 6, DMA & WS2812 LEDs

Summary

At Electronica last fall my project included two strips of WS2812B LEDs.  These LEDs were controlled by a PSoC 4 UDB component that my friend Mark Hastings built.  Here is a link to his project called FunWithLEDs.  That was cool and everything but I wanted to drive the LEDs from my PSoC 6.  The problem that I have is that there are no UDBs in the PSoC 6-2M and I’m not really a bit-banging kind of guy… so this gave me a chance to use the PSoC 6 DMA.

In this article I will

  1. Show you how the WS2812B LEDs work
  2. Give you a schematic for a PSoC 6 to drive them
  3. Explain how to create and use a frame buffer for the LEDs
  4. Explain the PSoC 6 DMA
  5. Glue it all together in a project
  6. Take you through debugging the code (I made several bugs)
  7. Show you how to calculate and measure the performance
  8. Take you through another layer of debugging
  9. Explain DMA chaining
  10. Automatically send the frameBuffer
  11. Explain why I’m lucky it works

WS2812B

The WS2812B LED strips are an almost arbitrary length (not quite true – more on this later) string of pixels that can be cascaded together via a serial line like this:

If you look on the internet you can purchase these strings in a whole bunch of different configurations e.g. Mouser and Adafruit NeoPixel.

Each pixel actually has three individual LEDs – Red, Green and Blue.  And each LED is attached to an 8-bit PWM, giving you 256 values of intensity – which is also known as 24-bit color because of 3 LEDs times 8-bit color.  Sometimes this is also known as 16 Million Colors.  To set the color of an individual pixel you it send three bytes, one each for Red, Green and Blue.  When the LED receives the reset code it takes the most recent RGB values and turns it on.

The communication protocol is cool.  When communication starts, a pixel takes its Red, Green and Blue value from the data stream, then passes on the rest of the bytes to the next pixels.  Basically each pixel peels off three bytes, then passes the rest through.  This scheme allows you to have almost any length string of pixels.  In the picture below you can see that the first pixels sees 3×3 bytes.  The second sees 2×3 bytes and the final pixel only sees its three bytes.

The only thing that is a bit weird in this protocol is that a “bit 1” is actually encoded as a long pulse of 1 followed by a short pulse of 0.  And a “bit 0” is short pulse of 1 followed by a long pulse of 0.  Here is a picture from the datasheet:

There is tons of blab-blabbing on the internet about how hard the timing is to deal with inside of the driver chip.  Here is the exact requirement:

My guess is that most of the heart ache is a result of people trying to bit-bang the protocol, which causes them to have to create fairly accurate, short timing loops.

But that isn’t what I am going to do.  But, Alan, what are you going to do?  First, observe from the table above that a T1H is double a T0H and a T0L is almost exactly double a T0H.  That means that the fundamental unit of time in this system is 0.4uS.  Which made me think to use the PSoC 6 to drive 0’s and 1’s out to the DI line of the first pixel at a rate of 1/0.4uS = 2.5 MHz using the SPI port.  This will let me encode 1 as 110 and encode 0 as 100.

Schematic

To build this project I started by making a new PSoC Creator Project.  I chose PSoC Creator instead of Modus Toolbox because I wanted to be able to use PSoC 4 component as well as PSoC 6.  My schematic has a SPI connected to DMA.  The DMA is connected to an interrupt.  A digital pin called “Red” which is attached to P0[3] on the board.  And finally a UART connected to the KitProg bridge.

In the SPI Configuration I remove the MISO pin (because we dont need SPI input data).  I could also remove the SCLK pin, but I left it attached so that I could see what was happening with an oscilloscope (which I needed when I was trying to figure out the bugs)

The Basic SPI is configured with a data rate of 2500kbs which is also known as 0.4uS per bit, as well as an oversample rate of 4.  The state machine of the SPI needs an input clock which is at least 2x the data rate.  If the MISO was still attached it might need an even higher input clock rate in order to oversample the input.  Notice that I also have one slave select line, which I was using to trigger the oscilloscope.  In the real system, there is no slave to select so you can use a 0 there.


On the Advanced table I enable “Dma Trigger on Tx Output” with the Fifo set to “63”.  This will cause the DMA trigger to be asserted anytime the transmit FIFO falls below 63 bytes.

WS2812 Framebuffer Code

From the schematic above you can see that my plan is to DMA data out of a buffer into the SPI transmit FIFO.  Given that you need a 1 to be 110 and a 0 to be 100 this will require 3 output bytes for each RGV value.  And each pixel will take 3-bits per bit or 3*3*8 bits or 9 bytes total per pixel.

As an Application developer using the WS2812 library, it will be nice to have functions that let you think in 3-bytes (RGB) and the library will translate those values into the 9-bytes required by the SPI.  OK, lets build the library with that in mind.  First I make some defines to get rid of the magic numbers.  Then I make the frame buffer which will hold the 9-byte formatted information.  Notice that I have a define called “WS_OFFSET” which I will talk about in more detail later.

#define WS_ZOFFSET (1)
#define WS_ONE3  (0b110<<24)
#define WS_ZERO3 (0b100<<24)
#define WS_NUM_LEDS (5)
#define WS_SPI_BIT_PER_BIT (3)
#define WS_COLOR_PER_PIXEL (3)
#define WS_BYTES_PER_PIXEL (WS_SPI_BIT_PER_BIT * WS_COLOR_PER_PIXEL)

static uint8_t WS_frameBuffer[WS_NUM_LEDS*WS_BYTES_PER_PIXEL+WS_ZOFFSET];

The next thing I create is a function to turn a 1-byte input into the 3-byte output.  It takes 1 bit at a time and then or’s in either 110 or 100.  It returns a uint32_t … but with the first byte is set to 0x0.

// Function: convert3Code
// This function takes an 8-bit value representing a color
// and turns it into a WS2812 bit code... where 1=110 and 0=011
// 1 input byte turns into three output bytes of a uint32_t
uint32_t WS_convert3Code(uint8_t input)
{
    uint32_t rval=0;
    for(int i=0;i<8;i++)
    {
        if(input%2)
        {
            rval |= WS_ONE3;
        }
        else
        {
            rval |= WS_ZERO3;
        }
        rval = rval >> 3;
        
        input = input >> 1;
    }
    return rval;
}

This first helper function WS_setRGB  will configure a specific pixel in the frameBuffer to be the RGB value encoded as 9-bytes.  Notice that the order in the frameBuffer is Green, Red, Blue (not RGB order)

// Function: WS_setRGB
// Takes a position and a three byte rgb value and updates the WS_frameBuffer with the correct 9-bytes
void WS_setRGB(int led,uint8_t red, uint8_t green, uint8_t blue)
{
    
    typedef union {
    uint8_t bytes[4];
    uint32_t word;
    } WS_colorUnion;
    
    WS_colorUnion color;
    color.word = WS_convert3Code(green);
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+WS_ZOFFSET] = color.bytes[2];
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+1+WS_ZOFFSET] = color.bytes[1];
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+2+WS_ZOFFSET] = color.bytes[0];
    
    color.word = WS_convert3Code(red);
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+3+WS_ZOFFSET] = color.bytes[2];
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+4+WS_ZOFFSET] = color.bytes[1];
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+5+WS_ZOFFSET] = color.bytes[0];

    color.word = WS_convert3Code(blue);
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+6+WS_ZOFFSET] = color.bytes[2];
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+7+WS_ZOFFSET] = color.bytes[1];
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+8+WS_ZOFFSET] = color.bytes[0];
}

DMA Code & Configuration

The DMA configuration starts with the defaults.  The channel priority doesn’t matter as I am only using one channel in this design.  I left the default descriptors set to 1 … which actually turns out to be a problem later on in my debugging (more on this later)

The descriptor is configured like this.  The configuration includes:

  1. The trigger output is unused
  2. An interrupt at the end (which I dont use)
  3. No chaining (meaning only on descriptor)
  4. The channel is disabled at the end (which I reset in software)
  5. One transfer per trigger (1-byte –> 1 word)
  6. As long as the trigger is active keep triggering (retrigger immediately)
  7. My buffer has bytes… but the input to the FIFO is words so setup Byte–>Word
  8. Hardcoded to 4-bytes of transfer, but I change this with the firmware based on the size of the frameBuffer

The first bit of code is an interrupt service routine which is attached to the DMA chain.  When I was trying to debug this originally I used the ISR to toggle an LED when the DMA was complete.  Now it doesnt do anything, but I left it in case I want to add something later.

// This is the interrupt handler for the WS DMA
// It doesnt do anything... and is just a stub
static void WS_DMAComplete(void)
{
    Cy_DMA_Channel_ClearInterrupt(WS_DMA_HW, WS_DMA_DW_CHANNEL);   
}

The function WS_DMAConfigure is used to:

  1. Initializes the DMA Descriptor
  2. Initializes the interrupt (which doenst do anything)
  3. Enables the DMA block
static void WS_DMAConfigure(void)
{
    /* Initialize descriptor */
    Cy_DMA_Descriptor_Init(&WS_DMA_Descriptor_1, &WS_DMA_Descriptor_1_config);
     
    /* Set source and destination for descriptor 1 */
    Cy_DMA_Descriptor_SetSrcAddress(&WS_DMA_Descriptor_1, (uint8_t *)WS_frameBuffer);
    Cy_DMA_Descriptor_SetDstAddress(&WS_DMA_Descriptor_1, (void *)&WS_SPI_HW->TX_FIFO_WR);
    Cy_DMA_Descriptor_SetXloopDataCount(&WS_DMA_Descriptor_1,sizeof(WS_frameBuffer));
    
     /* Initialize and enable the interrupt from WS_DMA */
    Cy_SysInt_Init(&WS_DMA_INT_cfg, &WS_DMAComplete);
    NVIC_EnableIRQ(WS_DMA_INT_cfg.intrSrc);
    Cy_DMA_Channel_SetInterruptMask(WS_DMA_HW, WS_DMA_DW_CHANNEL, WS_DMA_INTR_MASK);
    
    Cy_DMA_Enable(WS_DMA_HW);    
}

To actually make the DMA “go”, the function WS_DMATrigger sets up the DMA Channel, and then enables it.

void WS_DMATrigger()
{
        /* Initialize the DMA channel */
    cy_stc_dma_channel_config_t channelConfig;	
    channelConfig.descriptor  = &WS_DMA_Descriptor_1;
    channelConfig.preemptable = WS_DMA_PREEMPTABLE;
    channelConfig.priority    = WS_DMA_PRIORITY;
    channelConfig.enable      = false;
    Cy_DMA_Channel_Init(WS_DMA_HW, WS_DMA_DW_CHANNEL, &channelConfig);
    Cy_DMA_Channel_Enable(WS_DMA_HW,WS_DMA_DW_CHANNEL);
}

Gluing it all together

Now that we have a complete library, the last thing to do is build a command line interface to test it.   Lines 299-306 just get things going.  Then lines 308-310 turn on the systick timer and set it to a 1ms period.  I use the timer to trigger the DMA every 33ms.  The last part allows me to type a character and have it test a part of the system.

int main(void)
{
    __enable_irq(); /* Enable global interrupts. */

    UART_1_Start();
    setvbuf( stdin, NULL, _IONBF, 0 ); 
    printf("Started\n");   
    WS_Start();
    
    Cy_SysTick_Init(CY_SYSTICK_CLOCK_SOURCE_CLK_IMO,8000);
    Cy_SysTick_Enable();
    Cy_SysTick_SetCallback(0,WS_SysTickHandler);
 
    for(;;)
    {
        char c=getchar();
        switch(c)
        {        
            case 'u':
                printf("Enable auto DMA updating\n");
                Cy_SysTick_SetCallback(0,WS_SysTickHandler);
            break;
            case 'U':
                printf("Disable auto DMA updating\n");
                Cy_SysTick_SetCallback(0,0);
            break;
            case 't':
                printf("Trigger DMA\n");
                WS_DMATrigger();
            break;        
            case 'r':
                WS_setRGB(0,0xFF,0,0);
                printf("Set LED0 Red\n");
                break;
           case 'g':
                WS_setRGB(0,0,0xFF,0);
                printf("Set LED0 Green\n");
                break;            
            case 'O':
                WS_setRange(0,WS_NUM_LEDS-1,0,0,0);
                printf("Turn off all LEDs\n");
                break;
            case 'o':
                WS_setRange(0,WS_NUM_LEDS-1,0xFF,0xFF,0xFF);
                printf("Turn on all LEDs\n");
                break;
            case 'b':
                WS_setRGB(0,0,0,0xFF);
                printf("Set LED0 Blue\n");
                break;        
            case 'R':
                WS_setRange(0,WS_NUM_LEDS-1,0x80,0,0);
                printf("Turn on all LEDs RED\n");
                break;
            case 'G':
                WS_setRange(0,WS_NUM_LEDS-1,0,0x80,0);
                printf("Turn on all LEDs Green\n");
                break;
            case 'B':
                WS_setRange(0,WS_NUM_LEDS-1,0,0,0x80);
                printf("Turn on all LEDs Blue\n");
                break;     
            case 'a':
                WS_initMixColorRGB();
                printf("Turn on all LEDs RGB Pattern\n");
                break;
            case '?':
                printf("u\tEnable Auto Update of LEDs\n");
                printf("U\tDisable Auto Update of LEDs\n");
                printf("t\tTrigger the DMA\n");
                printf("r\tSet the first pixel Red\n");
                printf("g\tSet the first pixel Green\n");
                printf("b\tSet the first pixel Blue\n");
                printf("O\tTurn off all of the pixels\n");
                printf("o\tSet the pixels to white full on\n");
                printf("R\tSet all of the pixels to Red\n");
                printf("G\tSet all of the pixels to Green\n");
                printf("B\tSet all of the pixels to Blue\n");
                printf("a\tSet pixels to repeating RGBRGB\n");
                printf("?\tHelp\n");
                break;
        }
    }
}

Why is it not working?

After all of that I programmed the PSoC…. and… I got nothing.  This made me very unhappy because I wondered what the cause was.  Specifically, I was worried that I didn’t understand how to talk to the WS2812Bs.

My first step in debugging the problem was to get out the oscilloscope.  When I captured the data here is what I got.  The blue trace is the MOSI line which is driving the LED string, and the yellow trace is the SPI clock.  You can see from the measurement that the clock pulses are 392ns = 0.39uS … thats good.  You can see in the picture that the first “pulse” on the blue line  is 2 clocks long, thats good, but it is immediately followed by a 2 clock long 0.  That isn’t good.

Hang on I need 110 for 1 and 100 for 0… but I got 110 then 011.  No good.  Here is the code with the bug

#define ONE3  (0b110<<24)
#define ZERO3 (0b011<<24)

Once I fix that to be

#define ONE3  (0b110<<24)
#define ZERO3 (0b100<<24)

Things look good now.

However, this bug is really stupid because I did a partial job putting in unit test.  Here is the stupid part.  I wrote this:

    setRGB(0,0,0x80,0);
  
    for(int i=0;i<10;i++)
    {
        printf("%02X ",frameBuffer[i]);
    }
    printf("\n");
    
    printf("Test 0x00 = %0X\n",convert3Code(0));
    printf("Test 0xFF = %0X\n",convert3Code(0xFF));
    printf("Test 0x80 = %0X\n",convert3Code(0x80));

Which yielded

What would have been much better would have been to do this:

    setRGB(0,0,0x80,0);
  
    for(int i=0;i<10;i++)
    {
        printf("%02X ",frameBuffer[i]);
    }
    printf("\n");
    
    printf("Test 0x00 = %0X\n",convert3Code(0));
    printf("Test 0xFF = %0X\n",convert3Code(0xFF));
    printf("Test 0x80 = %0X\n",convert3Code(0x80));
    
    CY_ASSERT(convert3Code(0x00) == 0b00000000100100100100100100100100);
    CY_ASSERT(convert3Code(0xFF) == 0b00000000110110110110110110110110);
    CY_ASSERT(convert3Code(0x80) == 0b00000000110100100100100100100100);

Which would have put me here when the assert failed:

How long does it take to run?

The next thing that I was curious about is how long does it take to dump one frameBuffer into the LED string.  Well, the simplest thing seems to be to setup a longer transaction (25 LEDs) and then measure.  In the screen shot below you can see that is 721 uS for 25 LEDs or about 29uS per LED.

Which I suppose makes sense as 1 LED is 9 bytes or 9*8=72 bits at 0.4uS per but = 28.8uS.  This means 1000 LEDs is about 29 milliseconds.  Which means that you can easily do 1000 LEDs at 30Hz.

Why is it “yellow”?

As I was testing the runtime I noticed that the first LED of the chain was always yellow.  Why is that? (Notice that the LEDs are too bright to take a picture of and you can really only see the yellow in the reflection)

When I looked at the oscilloscope trace I notice that the pulse width of the first “1” was 1.2uS (the blue line is the data).  This is a result of the Serial Communication Block SPI pulling up the MOSI line a good while before it enables the chip select (the purple trace) which has the effect of making a very long 1 to start with.  This obviously is not a problem for the SPI protocol as it is clocked by the serial clock line.  But in this case where the LEDs are self-clocked it makes for a long 1.

OK, how do I fix that?  The cheap way to fix this is to make the first bit of the sequence a 0.  But, that would be a major pain because that would effectively shift every bit in the frame buffer over by 1-bit which is a pain in the ass.  So, to fix it I just make the entire first byte of the buffer be 0 which means it takes an extra byte and 8×0.4 uS = 3.2uS longer.  No big deal.

The code change is to add 1 byte to the buffer (which I called the WS_ZOFFSET)

#define WS_ZOFFSET (1)
#define WS_ONE3  (0b110<<24)
#define WS_ZERO3 (0b100<<24)
#define WS_NUM_LEDS (5)
#define WS_SPI_BIT_PER_BIT (3)
#define WS_COLOR_PER_PIXEL (3)
#define WS_BYTES_PER_PIXEL (WS_SPI_BIT_PER_BIT * WS_COLOR_PER_PIXEL)

static uint8_t WS_frameBuffer[WS_NUM_LEDS*WS_BYTES_PER_PIXEL+WS_ZOFFSET];

Then offset all of the writes by WS_ZOFFSET

// Function: WS_setRGB
// Takes a position and a three byte rgb value and updates the WS_frameBuffer with the correct 9-bytes
void WS_setRGB(int led,uint8_t red, uint8_t green, uint8_t blue)
{
    
    typedef union {
    uint8_t bytes[4];
    uint32_t word;
    } WS_colorUnion;
    
    WS_colorUnion color;
    color.word = WS_convert3Code(green);
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+WS_ZOFFSET] = color.bytes[2];
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+1+WS_ZOFFSET] = color.bytes[1];
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+2+WS_ZOFFSET] = color.bytes[0];
    
    color.word = WS_convert3Code(red);
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+3+WS_ZOFFSET] = color.bytes[2];
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+4+WS_ZOFFSET] = color.bytes[1];
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+5+WS_ZOFFSET] = color.bytes[0];

    color.word = WS_convert3Code(blue);
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+6+WS_ZOFFSET] = color.bytes[2];
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+7+WS_ZOFFSET] = color.bytes[1];
    WS_frameBuffer[led*WS_BYTES_PER_PIXEL+8+WS_ZOFFSET] = color.bytes[0];
}

Why did it crash?

While I was testing the runtime of the DMA loop, I kept increasing the number of LEDs.  When I typed 30, the PSoC crashed and was no longer responsive.  When this happens it is always useful to click on “Debug->Attach to Running Target”.  When I did that I ended up with this screen.  This tells me that there was an assert that was triggered.  When I hovered over the CY_DMA_IS_COUNT_VALID I realized that you can only DMA 256 bytes per descriptor… and with 30 LEDs I need 270 bytes… which means that I need to chain descriptors.

Chaining DMA Descriptors

You are allowed to create multiple DMA descriptors in the component configuration like this:

Which will give you three descriptors to configure.

And when you build the code you will find configuration structures for each of the descriptors in the generated source.  Here is a clip of code from then WS_DMA.C (which PSoC Creator generates)

const cy_stc_dma_descriptor_config_t WS_DMA_Descriptor_2_config =
{
    .retrigger       = CY_DMA_RETRIG_IM,
    .interruptType   = CY_DMA_1ELEMENT,
    .triggerOutType  = CY_DMA_1ELEMENT,
    .channelState    = CY_DMA_CHANNEL_ENABLED,
    .triggerInType   = CY_DMA_1ELEMENT,
    .dataSize        = CY_DMA_WORD,
    .srcTransferSize = CY_DMA_TRANSFER_SIZE_DATA,
    .dstTransferSize = CY_DMA_TRANSFER_SIZE_DATA,
    .descriptorType  = CY_DMA_SINGLE_TRANSFER,
    .srcAddress      = NULL,
    .dstAddress      = NULL,
    .srcXincrement   = 1L,
    .dstXincrement   = 1L,
    .xCount          = 1UL,
    .srcYincrement   = 1L,
    .dstYincrement   = 1L,
    .yCount          = 1UL,
    .nextDescriptor  = NULL
};

cy_stc_dma_descriptor_t WS_DMA_Descriptor_2 =
{
    .ctl = 0UL,
    .src = 0UL,
    .dst = 0UL,
    .xCtl = 0UL,
    .yCtl = 0UL,
    .nextPtr = 0UL
};

The problem is that it will be much better for the user of the WS2812 library to be able to configure the number of LEDs have it adjust the DMA chain automatically.  So, to chain the DMA Descriptors together I created my own descriptor initialization code:

  1. Calculate the number of descriptors required by looking at the sizeof the WS_frameBuffer.  You need to have at least 1.
  2. Create an array with enough descriptors
  3. Copy one of the descriptor initialization structures into my code (from the generated source), so that I can use the Cy_DMA_Descriptor_Init function
  4. Loop through all of the descriptors and initialize them.  Notice that I make the next descriptor be the next descriptor in the array
  5. The last descriptor will have less bytes, no next descriptor and you want to disable the channel when it is done
#define WS_NUM_DESCRIPTORS (sizeof(WS_frameBuffer) / 256 + 1)
static cy_stc_dma_descriptor_t WSDescriptors[WS_NUM_DESCRIPTORS];


static void WS_DMAConfigure(void)
{
    // I copies this structure from the PSoC Creator Component configuration 
    // in generated source
    const cy_stc_dma_descriptor_config_t WS_DMA_Descriptors_config =
    {
    .retrigger       = CY_DMA_RETRIG_IM,
    .interruptType   = CY_DMA_DESCR_CHAIN,
    .triggerOutType  = CY_DMA_1ELEMENT,
    .channelState    = CY_DMA_CHANNEL_ENABLED,
    .triggerInType   = CY_DMA_1ELEMENT,
    .dataSize        = CY_DMA_BYTE,
    .srcTransferSize = CY_DMA_TRANSFER_SIZE_DATA,
    .dstTransferSize = CY_DMA_TRANSFER_SIZE_WORD,
    .descriptorType  = CY_DMA_1D_TRANSFER,
    .srcAddress      = NULL,
    .dstAddress      = NULL,
    .srcXincrement   = 1L,
    .dstXincrement   = 0L,
    .xCount          = 256UL,
    .srcYincrement   = 0L,
    .dstYincrement   = 0L,
    .yCount          = 1UL,
    .nextDescriptor  = 0
    };

    for(unsigned int i=0;i<WS_NUM_DESCRIPTORS;i++)
    {
        Cy_DMA_Descriptor_Init(&WSDescriptors[i], &WS_DMA_Descriptors_config);
        Cy_DMA_Descriptor_SetSrcAddress(&WSDescriptors[i], (uint8_t *)&WS_frameBuffer[i*256]);
        Cy_DMA_Descriptor_SetDstAddress(&WSDescriptors[i], (void *)&WS_SPI_HW->TX_FIFO_WR);
        Cy_DMA_Descriptor_SetXloopDataCount(&WSDescriptors[i],256); // the last
        Cy_DMA_Descriptor_SetNextDescriptor(&WSDescriptors[i],&WSDescriptors[i+1]);
    }
    
    // The last one needs a bit of change
    Cy_DMA_Descriptor_SetXloopDataCount(&WSDescriptors[WS_NUM_DESCRIPTORS-1],sizeof(WS_frameBuffer)-256*(WS_NUM_DESCRIPTORS-1)); // the last
    Cy_DMA_Descriptor_SetNextDescriptor(&WSDescriptors[WS_NUM_DESCRIPTORS-1],0);
    Cy_DMA_Descriptor_SetChannelState(&WSDescriptors[WS_NUM_DESCRIPTORS-1],CY_DMA_CHANNEL_DISABLED);
 
    
     /* Initialize and enable the interrupt from WS_DMA */
    Cy_SysInt_Init(&WS_DMA_INT_cfg, &WS_DMAComplete);
    NVIC_EnableIRQ(WS_DMA_INT_cfg.intrSrc);
    Cy_DMA_Channel_SetInterruptMask(WS_DMA_HW, WS_DMA_DW_CHANNEL, WS_DMA_INTR_MASK);
    
    Cy_DMA_Enable(WS_DMA_HW);    
}

With all of that done, the last change is to change the DMA channel initialization code to use the correct first descriptor

void WS_DMATrigger()
{
    cy_stc_dma_channel_config_t channelConfig;	
    channelConfig.descriptor  = &WSDescriptors[0];
    channelConfig.preemptable = WS_DMA_PREEMPTABLE;
    channelConfig.priority    = WS_DMA_PRIORITY;
    channelConfig.enable      = false;
    Cy_DMA_Channel_Init(WS_DMA_HW, WS_DMA_DW_CHANNEL, &channelConfig);
    Cy_DMA_Channel_Enable(WS_DMA_HW,WS_DMA_DW_CHANNEL);
}

Automatically send the Frame Buffer

I initially “triggered” the DMA using keyboard commands that called the function WS_DMATrigger.  But after I got things working I realized that what I really wanted to do was update the “screen” aka the strip of LEDs at about 30Hz.  To do this I turn on the ARM SysTick timer to call my function WS_SysTickHandler every 1ms.

    Cy_SysTick_Init(CY_SYSTICK_CLOCK_SOURCE_CLK_IMO,8000);
    Cy_SysTick_Enable();

In the WS_SysTickHandler, I count the number of times I have been called and store it in a static variable called “count”.  When count gets to 33 (or greater) meaning that 33mS has gone by, I check to make sure that the DMA is disabled and then I call the update function.  I also reset the timer back to 0.

void WS_SysTickHandler()
{
    static int count=0;
    if((Cy_DMA_Channel_GetStatus(WS_DMA_HW,WS_DMA_DW_CHANNEL) & CY_DMA_INTR_CAUSE_COMPLETION) && count++>32)
    {
        WS_DMATrigger();
        count = 0;
    }
}

With that installed I get a nice 30Hz update.  The screenshot below is configured with 144 Pixels and it take 4.15mS to update the screen which is about a 12% duty cycle.

I’m Lucky it Works

The last thing to observe in all of this is that I am driving the LED string with a 5V wall wart.

And according to the datasheet VIH is 0x7 * VDD = 3.5V … and I am driving it with a PSoC 6 with 3.3V.  Oh well.

All of this source code is available at GitHub or you can clone git@github.com:iotexpert/WS2812.git

BLE Write Request, Write Command, Signed Write Command & Prepare Write

Summary

In my article entitled “PSoC4 BLE Central Custom Profile w/LED & CapSense” a reader named Doug posted a question which asked about BLE Writes and the events that are created in PSoC 4 BLE.  This article attempts to answer those questions.  The bottom line is that the BLE Spec specifies four ways for a GATT Client to write to a GATT Server.  Each of those write types either have a response or not.  Inside of the PSoC BLE Stack each of them generate a different event.

Here is a summary of the BLE Write Events from my WICED BLE textbook chapter 4D.4

[EDIT: The above table is incorrect.  3.4.5.1 is ATT_WRITE_REQ which requires a 3.4.5.2 an ATT_WRITE_RSP.   And 3.4.5.3 is ATT_WRITE_CMD which requires no response.]

Inside of the PSoC Stack it will generate the following events:

Request Event PSoC 4 Response API
Write Request CYBLE_EVT_GATTS_WRITE_REQ CyBle_GattsWriteRsp or CyBle_GattsErrorRsp
Write Command CYBLE_EVT_GATTS_WRITE_CMD_REQ No Response
Signed Write CYBLE_EVT_GATTS_DATA_SIGNED_CMD_REQ CyBle_GattsWriteRsp or CyBle_GattsErrorRsp
Prepare Write CYBLE_EVT_GATTS_PREP_WRITE_REQ Out of scope for today
Execute Write CYBLE_EVT_GATTS_EXEC_WRITE_REQ Out of scope for today

Question(s)

Here are the original question(s):

Hi, I am having some problems understanding “write without response” setup.
In the “capsenseled” server project “CYBLE_EVT_GATTS_WRITE_REQ” is used instead of “CYBLE_EVT_GATTS_WRITE_CMD_REQ” in the event handler, but “write without response” is selected in the component setup.
According to this other project I am looking at ( https://www.digikey.com/eewiki/pages/viewpage.action?pageId=55574662 ) both are used with comments as follows

case CYBLE_EVT_GATTS_WRITE_REQ: //Write with response
case CYBLE_EVT_GATTS_WRITE_CMD_REQ: //Write without response

Why is “CYBLE_EVT_GATTS_WRITE_CMD_REQ” NOT used in the capsenseled project, and does “CYBLE_EVT_GATTS_WRITE_REQ” still function in a write without response setup?
Am I misunderstanding the definition of a response?
Is the “response” referring to an event that occurs within the server device, or is a response defined as a reply from the client device?

I am trying to set up a low power single button remote to report the state of the button, to be handled by the client which is connected to a larger power source and will possibly count edges, measure button press duration, or both, etc. Ideally the remote would be asleep except for when it needs to tell the client that the button has changed states, and would not spend any energy processing a response from the client.

BLE Spec

When you declare a Characteristic, you have a 1-byte bit field to specify the Characteristic Properties, that tells the GATT Client how it can write to the Characteristic.  Here is a screenshot from section 3.3.1 of the Bluetooth core spec:

And the legal values for the Characteristic Properties are in section 3.3.1.1

This table says that it is perfectly legal to perform a “Write without response” (which is really called “Write Command” in section 3.4.5.3) or a “Write” (which is really called “Write Request” in section 3.4.5.1) as long as you send a legal response (or none).  If you look at the project which is referenced in the Digikey article, you can see that the author did just that for the Characteristic “Number_Write”.

Write Command a.k.a. Write Without Response”

As I looked around at the spec for this article I discovered something which I did not previously know.  Specifically, a Write Command must fit in one packet… which means that the data written to the Characteristic must be no more than the ATT MTU – 3.  Here is the spec:

Write Request

A Write Request is a two sided request, meaning the GATT Server must either send a “Write Response” or an “Error Response”  Here is a screenshot from the spec.

Write Response

A Write Response is a stupid simple packet with just one possible value… 0x13 meaning that the write worked.  In the PSoC, you generate this packet by calling “CyBle_GattsWriteRsp” with the connection handle as the only parameter.

Error Response

The more interesting response is an error where you are allowed to send more information.  To generate an error response in PSoC you call CyBle_GattsErrorRsp with the connection handle and the error code.

The spec gives a list of legal error codes.  Here is a snapshot from the spec with a FEW of them.

The interesting thing about these error codes is that you are allowed to create you own “Application Errors” which will have your application semantics.

PSoC Creator has an enumerated list for the error codes (here is a short section from the BLE Component Datasheet)

Answers

OK… so that leaves me with giving specific answers to his questions.  My answer are embedded in bold:

Hi, I am having some problems understanding “write without response” setup.
In the “capsenseled” server project “CYBLE_EVT_GATTS_WRITE_REQ” is used instead of “CYBLE_EVT_GATTS_WRITE_CMD_REQ” in the event handler, but “write without response” is selected in the component setup.
According to this other project I am looking at ( https://www.digikey.com/eewiki/pages/viewpage.action?pageId=55574662 ) both are used with comments as follows

case CYBLE_EVT_GATTS_WRITE_REQ: //Write with response
case CYBLE_EVT_GATTS_WRITE_CMD_REQ: //Write without response

Why is “CYBLE_EVT_GATTS_WRITE_CMD_REQ” NOT used in the capsenseled project,

When I wrote that project originally I chose to only support no responses to writes.  There is no need to support both.  As the application developer you can choose to do either or both.

and does “CYBLE_EVT_GATTS_WRITE_REQ” still function in a write without response setup?

If the GATT Client sends you a CYBLE_EVT_GATTS_WRITE_REQ and you send a response it might turn out badly and you should not do that.
Am I misunderstanding the definition of a response?
Is the “response” referring to an event that occurs within the server device, or is a response defined as a reply from the client device?

Well..  sounds like the answer was yes… but hopefully it is no now.

I am trying to set up a low power single button remote to report the state of the button, to be handled by the client which is connected to a larger power source and will possibly count edges, measure button press duration, or both, etc. Ideally the remote would be asleep except for when it needs to tell the client that the button has changed states, and would not spend any energy processing a response from the client.

Sounds like “Write without response” is perfect for your application.

Embedded Graphics: TFT Displays & Drivers

Summary

This article will take you through a high level overview of all of the parts of a TFT LCD display.  The vast majority of what I have read on the internet makes this whole issue massively complex.  I’m quite sure that this complexity problem is a real reflection of the serious design and manufacturing complexity in these displays and drivers.  That being said, to get a conceptual understanding is much simpler, and is the point of this article.

A significant amount of my learning about this subject came from a 195 page powerpoint presentation by Dr. Fang-Hsing Wang entitled “Flat Panel Display : Principle and Driving Circuit Design“.  He has graciously allowed me to reproduce a few of his images.  This dude knows way way more about these circuits than I do and I would encourage you to read his work.

This article has the following subsections:

  1. TFT Pixel
  2. TFT Pixel Schematic
  3. TFT Panels (Also known as TFT Glass)
  4. TFT Gate Drivers
  5. TFT Source Drivers
  6. Gamma
  7. Multiplexing Gate and Source Drivers

TFT Pixel

The fundamental element in a TFT display is the liquid crystal.  These elements have the property that the crystals will align from horizontal (which blocks the light) to vertical (which lets most of the light through) based on the electric field applied to them.  Basically, you shine light through the liquid crystal, which blocks some or all of the light, the remainder of the white light then goes through a color filter to make red, green, or blue. It works like this:

  1. You use an array of LEDs to shine white light from the back of the screen towards the front (your eyes) 
  2. Into a diffuser (to spread out the light and make it even)
  3. You control the orientation of the crystals using a voltage to apply an electric field to the crystals
  4. The white light from the back (often called the backlight) will shine through the liquid crystal elements.  The amount of light coming out will depend on the orientation of the crystals.
  5. The white light coming out of the crystal will then go into a red, green or blue color filter making it red, green or blue (RGB)
  6. The light from three RGB filters will combine in your eye into a color based on the amount of red, green and blue (purple in the case below)

This architecture means that every pixel in the display will require a red, green and blue element.  And, you will need to control the voltage on all of the elements (which will be quite a lot on a screen of any size)

Here is a nice cross section that I found on Innolux’s website.

Pixel Schematic

What does the schematic for one element in a pixel look like?  And where is the T(transistor) in the TFT?  The three letter acronym TFT stands for a thin film transistor that is physically on the top of the LCD matrix right next to each liquid crystal element.  Here is a schematic model for one element in the array.  C-LC represents the capacitance of the liquid crystal.  CS is a storage capacitor that is used to hold the electric field across the liquid crystal when the transistor is OFF.   To apply a voltage across the LC you just turn on the gate and apply the correct voltage to the column commonly known as the source.

You should notice that the “back” terminal of the two capacitors is called “VCOM” and is physically on the other side of the liquid crystal matrix from the TFT.  All of the liquid crystal backsides in the display are connected to the same VCOM.  A bit of painfulness in this system is that the CS capacitor leaks, which means that the LCD changes state which means that each pixel must be updated, properly called refreshed, on a regular basis.

TFT Panel

We know that each pixel has three three thin film transistors, three capacitors, three color filters (red, green and blue) and that we need to control the voltage on the source/drain of each transistor in order to cause the right amount of light to come through the liquid crystal.  How do we do that?  The first step is to arrange all of the pixels in a matrix.  Each row of matrix has all of the gates connected together.  And each column of the matrix has all of the sources tied together.  In order to address a specific pixel RGB element, you turn on the correct row and then apply a voltage to the correct column at the right time.

If you have been thinking about this system you might have done a little bit of math and figured out that you are going to need an absolute boatload of source and gate driver signals.  And you would be right!  For example, a 4.3″ screen with 480×272 will require 480x272x3 elements which are probably organized into 480 rows by 816 columns.  This would require a chip with at least 480+816=1296 pins, that is a lot.  It turns out that for small screens <=3.5″ there are chips with enough pins to do the job.  But, for larger screens, it requires multiple chips to do the job.  The “…” in the picture above shows the driver chips being cascaded.  The next thing to know is that “TFT Glass” usually has the driver chip(s) embedded into the screen at the edge (you can see that in the picture from Innolux above).

TFT Gate Drivers

You must put a quite high voltage source >20v and drain <-10V across the liquid crystal at the right time to get it to do its thing.  In order to pass that source voltage, the gate must be turned on at the right time to the right voltage, this is the purpose of the Gate Driver IC.  The gate driver is conceptually simple and Dr. Wang drew a nice picture on page 7 of his presentation.  You can see that it is basically a shift register, with one element per gate.  You shift in a “1” and then clock it through the entire shift register which will have the effect of applying a 1 to each gate.

However, a 3.3v logic 1 is not anywhere high enough to drive the gate so that it can pass the much higher source voltage.  So, you need to level shifter and a buffer to get the “right” voltage.   On page 15, Dr. Wang made a nice picture of this circuit as well.

It turns out that this picture is conceptually correct, but the exact implementation has “a lot going on”.  You can read about the next layer of circuit design in his presentation on pages 15-35.

TFT Source Drivers

In its most basic form, the TFT source driver is responsible for taking an 8-bit digital input value representing the value of an individual LCD element and turning it into a voltage, the driving the voltage.  Like this:

You could conceptually have one DAC per column in the panel.  But this would have at least two problems

  1. The DACs are big circuits and this would make for giant source driver chips
  2. You would need to “save” all of the digital values for an entire row so that when you turned on the row, you could turn on all of the DACs on at the same time.

You could conceptually also have one DAC for all of the columns, but this would have a bunch of problems including:

  1. The DAC would have to be strong enough to drive all of the columns
  2. You would need 3x the number of row drivers to effectively de-mux the column
  3. You would need 1 pin on the source driver per column in the panel (for an 800×600 lcd that would be 600×3 = 1800 pins)

In reality there is some compromise of chip size, number of pins and time that is made by multiplexing pins, columns and rows.  For example, many of the small screens appear to have 1 column driver for all of the reds, 1 driver for the blues and one for greens. 

What appears to happen in real life on bigger screens is some combination of column and row multiplexing.  In one display that I found there were 2x the number of rows which allows the columns to be multiplexed 2-1.  The display is 1024×600.  That requires 1024*3 RGBs in the column = 1536 pins.  This means that you need to double the number of gate drivers, resulting in 1200 pins in the row direction.  Here is a picture from their datasheet.

Gamma Correction

The last issue that I will address in TFT LCD drivers is called Gamma Correction or more simply Gamma.  Gamma is an intensity adjustment factor.  For any given digital intensity input, you will need a non-linear translation to a voltage output on the source.  For example a doubling of digital input (so that a pixel appears twice as bright) you will not double but instead will have some non-linear translation of the output voltage.

There appear to be a bunch of reason why you need Gamma Correction including at least:

  1. Your eye perceives light intensity in a non-linear way
  2. The LCD panel responds differently based on the input
  3. The intensity variance is dependent on the color

The good news is that this gamma correction is built into the display drivers.  From my reading, this is sometimes done with digital processing, and sometimes done with an analog circuit.  But in general, it appears to be tuned and programmed into the driver by the panel vendor for these smaller display.

In the next article I will write about TFT Controllers.

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

MCU Friend 3.5 TFT Identification

Summary

I bought four MCU Friend 3.5″ TFT shields.  And, unfortunately, they have spiraled me into a deep, dark place trying to figure out how to use them.  The the documentation consists of a sticker on the antistatic bag, a picture of the shield with a list of 5 different possible LCD drivers, a pinout, and a block of code that supposedly represents the startup code.  The unfortunate part is that none of these have been exactly right – they all have errors.  This article is a description of the journey to figuring out how to use them.

This article has the following parts:

  1. MCU Friend 3.5″ Documentation
  2. MCU Friend 3.5″ Shields
  3. Using the MCUFRIEND_kbv Library
  4. Identify the LCD Driver with Register Reads
  5. A PSoC Program To Identify LCD Controllers
  6. Using the MCUFriend_kbv Startup Code
  7. Use the Web Startup
  8. Conclusion

MCU Friend 3.5″ Documentation

Here is a picture of the bag. (the QR code is a number “181024202132” which I thought might be a phone number but isn’t.  It also doesn’t match anything in google, so i’m not sure what it is.

This text on the website says:

Features:

  •   5inch TFT LCD Module, Resolution 480×320, Controller ili9481 ili9468, ili9488 hx8357, or r61581.
  •      Designed with a TF(Micro SD) card socket on the back of board so that you can conveniently insert a card.
  •      Support touch screen function.
  •      The test code is provided below.
  •      This kit requires certain professional knowledge and ability, make sure you know how to use it, please. We cannot provide any technical assistance.

Specifications:

Controller: ili9481 ili9468, ili9488 hx8357, or r61581

Resolution: 480×320

Voltage: 5V/3.3V

Package Include: 1 x LCD Module

The website also has this code which they claim is the startup code.  It is interesting that

  1. There are several lines are commented out
  2. It implies that you have a  SPI interface
  3. When you look at the commands some of them don’t exist in some of the controllers
write_SPI_commond(0xFF);
write_SPI_commond(0xFF);
delay_nms(5);
write_SPI_commond(0xFF);
write_SPI_commond(0xFF);
write_SPI_commond(0xFF);
write_SPI_commond(0xFF);
delay_nms(10);
write_SPI_commond(0xB0);
write_SPI_data(0x00);
write_SPI_commond(0xB3);
write_SPI_data(0x02);
write_SPI_data(0x00);
write_SPI_data(0x00);
write_SPI_data(0x10);
write_SPI_commond(0xB4);
write_SPI_data(0x11);//0X10
write_SPI_commond(0xC0);
write_SPI_data(0x13);
write_SPI_data(0x3B);//
write_SPI_data(0x00);
write_SPI_data(0x00);
write_SPI_data(0x00);
write_SPI_data(0x01);
write_SPI_data(0x00);//NW
write_SPI_data(0x43);
write_SPI_commond(0xC1);
write_SPI_data(0x08);
write_SPI_data(0x15);//CLOCK
write_SPI_data(0x08);
write_SPI_data(0x08);
write_SPI_commond(0xC4);
write_SPI_data(0x15);
write_SPI_data(0x03);
write_SPI_data(0x03);
write_SPI_data(0x01);
write_SPI_commond(0xC6);
write_SPI_data(0x02);
write_SPI_commond(0xC8);
write_SPI_data(0x0c);
write_SPI_data(0x05);
write_SPI_data(0x0A);//0X12
write_SPI_data(0x6B);//0x7D
write_SPI_data(0x04);
write_SPI_data(0x06);//0x08
write_SPI_data(0x15);//0x0A
write_SPI_data(0x10);
write_SPI_data(0x00);
write_SPI_data(0x31);//0x23
write_SPI_data(0x10);
write_SPI_data(0x15);//0x0A
write_SPI_data(0x06);//0x08
write_SPI_data(0x64);//0x74
write_SPI_data(0x0D);//0x0B
write_SPI_data(0x0A);//0x12
write_SPI_data(0x05);//0x08
write_SPI_data(0x0C);//0x06
write_SPI_data(0x31);//0x23
write_SPI_data(0x00);
write_SPI_commond(0x35);
write_SPI_data(0x00);
//write_SPI_commond(0x36);
//write_SPI_data(0x00);
write_SPI_commond(0x0C);
write_SPI_data(0x66);
write_SPI_commond(0x3A);
write_SPI_data(0x66);
write_SPI_commond(0x44);
write_SPI_data(0x00);
write_SPI_data(0x01);
write_SPI_commond(0xD0);
write_SPI_data(0x07);
write_SPI_data(0x07);//VCI1
write_SPI_data(0x14);//VRH 0x1D
write_SPI_data(0xA2);//BT 0x06
write_SPI_commond(0xD1);
write_SPI_data(0x03);
write_SPI_data(0x5A);//VCM  0x5A
write_SPI_data(0x10);//VDV
write_SPI_commond(0xD2);
write_SPI_data(0x03);
write_SPI_data(0x04);//0x24
write_SPI_data(0x04);
write_SPI_commond(0x11);
delay_nms(150);
write_SPI_commond(0x2A);
write_SPI_data(0x00);
write_SPI_data(0x00);
write_SPI_data(0x01);
write_SPI_data(0x3F);//320
write_SPI_commond(0x2B);
write_SPI_data(0x00);
write_SPI_data(0x00);
write_SPI_data(0x01);
write_SPI_data(0xDF);//480
//write_SPI_commond(0xB4);
//write_SPI_data(0x00);
delay_nms(100);
write_SPI_commond(0x29);
delay_nms(30);
write_SPI_commond(0x2C);

It also has a picture which says the LCD has one of several different controllers (and after digging in I know for a fact that two of mine were made by Raydium and are not on the list)

And finally a table of pins.  Which is interesting as it lists 37 pins when the shield has no where near that number.  And it shows the shield as  16-bit interface which it isnt … and it shows some LEDs which aren’t there either.

MCU Friend 3.5″ Shields

I bought 4 different shields.  One came broken.  The other three are all different.  When you look at the boards there are two visibly different configurations

  

Using the MCUFRIEND_kbv Library

The first thing I did was try to use the MCUFRIEND_kbv library to see if the screens worked.  The first board identified as ID=0x9403 and did not work.  Apparently, the tool just spits out the ID if it doesn’t know it, which it did not.

One of the boards identified as ID=0x6814 worked perfectly, and one had a blue cast to all of the screens.  The crazy part is the two boards that identified as ID=0x6814 had different PCBs.  According to the comments in the MCUFRIEND_kbv.cpp ID=0x6814 is an RM68140 and ID=9403 is unknown.

Here is the one with the blue cast:

Here is the functional one:

Identify with Register Reads

Next, I started down the path of trying to figure out what the controllers were by using register reads.  David Prentice (the guy who wrote/maintains the MCU Friend_kbv Arduino library) has an absolute ton of responses on the Arduino forum trying to help people figure out what their shield is.  He asks them to post the register report from his example program LCD_ID_readnew which is included as an example in the library.

When you look at these LCD controllers they all have some variant of “Read ID” which responds with 1-6 bytes.  The basic idea of this program is to look at what bytes are returned to try to identify the controller.  Here is an example of what I got when I ran the LCD_ID_readnew program on my shields:

reg(0x0000) 00 00       ID: ILI9320, ILI9325, ILI9335, ...
reg(0x0004) 54 54 80 66 Manufacturer ID
reg(0x0009) 00 00 61 00 00           Status Register
reg(0x000A) 08 08 Get Powsr Mode
reg(0x000C) 66 66 Get Pixel Format
reg(0x0030) 00 00 00 01 DF  PTLAR
reg(0x0033) 00 00 00 01 E0 00 00        VSCRLDEF
reg(0x0061) 00 00 RDID1 HX8347-G
reg(0x0062) 00 00 RDID2 HX8347-G
reg(0x0063) 00 00 RDID3 HX8347-G
reg(0x0064) 00 00 RDID1 HX8347-A
reg(0x0065) 00 00 RDID2 HX8347-A
reg(0x0066) 00 00 RDID3 HX8347-A
reg(0x0067) 00 00 RDID Himax HX8347-A
reg(0x0070) 00 00 Panel Himax HX8347-A
reg(0x00A1) 00 00 00 00 00    RD_DDB SSD1963
reg(0x00B0) 00 00 RGB Interface Signal Control
reg(0x00B3) 00 00 11 00 00      Frame Memory
reg(0x00B4) 00 00 Frame Mode
reg(0x00B6) 02 02 02 3B 00      Display Control
reg(0x00B7) 06 06 Entry Mode Set
reg(0x00BF) FF FF 68 14 00 FF   ILI9481, HX8357-B
reg(0x00C0) 0E 0E 0E 00 00 00 00 00 00   Panel Control
reg(0x00C1) 04 04 00 00 Display Timing
reg(0x00C5) 00 00 Frame Rate
reg(0x00C8) 00 00 00 00 00 00 00 00 00 00 00 00 00      GAMMA
reg(0x00CC) 00 00 Panel Control
reg(0x00D0) 00 00 00 00 Power Control
reg(0x00D1) 00 00 00 00 VCOM Control
reg(0x00D2) 00 00 00 Power Normal
reg(0x00D3) 00 00 94 86    ILI9341, ILI9488
reg(0x00D4) 00 00 00 00    Novatek
reg(0x00DA) 54 54 RDID1
reg(0x00DB) 80 80 RDID2
reg(0x00DC) 66 66 RDID3
reg(0x00E0) 00 00 54 07 44 05 08 00 54 07 44 05 08 44 44 00     GAMMA-P
reg(0x00E1) 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00     GAMMA-N
reg(0x00EF) 00 00 00 00 00 00 ILI9327
reg(0x00F2) 00 00 00 00 00 00 00 00 00 00 00 00 Adjust Control 2
reg(0x00F6) 00 00 00 00 Interface Control

The key thing to see in this output is the register 0x04 which says 54,80,66 which identifies this as a Raydium RM68140 LCD controller.  Here is a snapshot from the data sheet.

Unfortunately, the next thing to notice is that Register 0xBF has reg(0x00BF) FF FF 68 14 00 FF.  The unfortunate part is that this register is not documented in the data sheet beyond this one reference:

Presumably the “68 14” corresponds to a Raydium 68140, but who knows?  When I posted this on the Arduino forum, David Prentice responded (David does yeoman’s labor helping people and should be Thanked for all of his pro-bono work and putting up with a bunch of really bad questions)

After digging some more, I decided that it is super ugly out there, as you find that there are a significant number of LCD controllers that are clones, copies, pirated etc… and that they all present themselves differently.  And, in hindsight I think that this is the reason that my ILI9341 from the previous article doesnt quite work correctly.

A PSoC Program To Identify LCD Controllers

The next thing that I did was create a PSoC Program to read registers from the controllers to try to figure out what they were.  My original plan was to write a complete identification program, but I have largely decided that this is a waste of time (more on this later).  Here is the beginning of the project, it is called “Identify” in the workspace.

First, a function to reset the screen by toggling the reset line on the controller, then sending a command “0x01” which is commonly a software reset.   It turns out that I spent a bunch of time trying to figure out what was going on because I was not getting any responses from the controllers.  This was caused by not sending the software reset, which at least in two of the cases makes them unresponsive.

#define LCD_COMMAND (0)
#define LCD_DATA (1)

void lcdReset()
{
       /* Reset - High, Low (reset), High */
    Cy_GPIO_Set(LCD_RESET_N_0_PORT, LCD_RESET_N_0_NUM);
    CyDelay(200);
    Cy_GPIO_Clr(LCD_RESET_N_0_PORT, LCD_RESET_N_0_NUM);
    CyDelay(200);
    Cy_GPIO_Set(LCD_RESET_N_0_PORT, LCD_RESET_N_0_NUM);
    CyDelay(200);
   
    GraphicLCDIntf_1_Write8(LCD_COMMAND,0x01);
 
}

void regReadPrint(uint8_t reg,uint8_t *buff,uint16_t num)
{
    GraphicLCDIntf_1_Write8(LCD_COMMAND,reg);
    GraphicLCDIntf_1_ReadM8(LCD_DATA,buff,num);
    printf("%02X ",reg);
    for(int i=0;i<num;i++)
    {
        printf("0x%02X,",buff[i]);
    }
    printf("\r\n");
    
}

Then I built a command line interface that queries the typical registers:

int main(void)
{
    uint8_t buff[128];

    __enable_irq(); /* Enable global interrupts. */

    UART_Start();
    setvbuf( stdin, NULL, _IONBF, 0 );
    printf("Started\r\n");
    
    GraphicLCDIntf_1_Start();
    lcdReset();
    
    while(1)
    {
        char c;
        c=getchar();
        
        switch(c)
        {  
            case 'a':
                regReadPrint(0x04,buff,6);
                regReadPrint(0xA1,buff,6);
                regReadPrint(0xBF,buff,6);
                regReadPrint(0xDA,buff,1);
                regReadPrint(0xDB,buff,1);
                regReadPrint(0xDC,buff,1);
                regReadPrint(0xd3,buff,6);
            break;

When I ran this program on the three controllers here is what I got:

Screen 1:
04 0x00,0x00,0x94,0x03,0x00,0x00,
A1 0x00,0x00,0x00,0x00,0x00,0x00,
BF 0x00,0x00,0x00,0x00,0x00,0x00,
DA 0x00,
DB 0x94,
DC 0x03,
D3 0x00,0x00,0x94,0x03,0x00,0x00,

Screen 2: Raydium 68140 (arduino works)
04 0x54,0x54,0x80,0x66,0x00,0x00,
BF 0xFF,0xFF,0x68,0x14,0x00,0xFF,
DA 0x54,
DB 0x80,
DC 0x66,
D3 0x00,0x00,0x94,0x86,0x00,0x00,

Screen 3: Raydium 68140 (looks blue)
04 0x54,0x54,0x80,0x66,0x00,0x00,
BF 0xFF,0xF7,0x60,0x14,0x00,0xFF,
DA 0x54,
DB 0x80,
DC 0x66,
D3 0x00,0x00,0x94,0x86,0x00,0x00,

So, where does this leave me?

  1. I have no idea what Screen 1 is?  04 0x00,0x00,0x94,0x03,0x00,0x00,
  2. Two of them appear to be Raydium RM68140s
  3. The two Raydiums have different register values for 0xBF

And all of this is insane because most of these companies don’t appear to have coherent websites or generally available datasheets.  I suppose that it would help if I spoke and read Chinese.

Using the MCUFriend_kbv Startup Code

The next thing that I did was try out the startup code that MCUFriend_kbv generates.  I used the same technique from PSoC 6 + Segger EmWin + MCUFriend 2.4″ Part 1 and spit out the startup bytes.  Here they are:

0x1,0x0, 
0x28,0x0, 
0x3A,0x1,0x55, 
0x3A,0x1,0x55, 
0x11,0x0, 
0x29,0x0, 
0xB6,0x3,0x0,0x22,0x3B, 
0x36,0x1,0x8, 
0x2A,0x4,0x0,0x0,0x1,0x3F, 
0x2B,0x4,0x0,0x0,0x1,0xDF, 
0x33,0x6,0x0,0x0,0x1,0xE0,0x0,0x0, 
0x37,0x2,0x0,0x0, 
0x13,0x0, 
0x20,0x0, 

And this is what it looks like in my PSoC program:

static const uint8_t mcu35_init_sequence_kbv[]  = {
    0x1,0x0,                            // Software Reset
    0x28,0x0,                           // Display Off
    0x3A,0x1,0x55,                      // Pixel Format Set 565
    0x3A,0x1,0x55,                      // Pixel Format Set 565
    0x11,0x0,                           // Sleep Out
    0x29,0x0,                           // Display On
    0xB6,0x3,0x0,0x22,0x3B,             // Display Function Control
    0x36,0x1,0x8,                       // Memory Access Control
    0x2A,0x4,0x0,0x0,0x1,0x3F,          // Column Set Address 320
    0x2B,0x4,0x0,0x0,0x1,0xDF,          // Page Set Addres 480
    0x33,0x6,0x0,0x0,0x1,0xE0,0x0,0x0,  // Vertical Scrolling Definition
    0x37,0x2,0x0,0x0,                   // Vertical Scrolling Start Address
    0x13,0x0,                           // Normal Display On
    0x20,0x0,                           // Display Inversion Off
};

When I run this things look like this:

Screen 1: Looks good, just need to flip the x-axis

Screen 2: Looks good, just need to flip the y-axis

Screen 3: Not good… not exactly sure how to fix.

Use the Web Startup

Well, things still aren’t quite right, so for some strange reason, I keep going and try to use the startup code from the web.  In order to make it work I translate

  • delay_nms –> CyDelay
  • write_SPI_commond –> GraphicLCDIntf_1_Write8_A0
  • write_SPI_data –> GraphicLCDIntf_1_Write8_A1

Here is the updated code:

static void _InitController35Web()
{
    GraphicLCDIntf_1_Write8_A0(0xFF);
    GraphicLCDIntf_1_Write8_A0(0xFF);
    CyDelay(5);
    GraphicLCDIntf_1_Write8_A0(0xFF);
    GraphicLCDIntf_1_Write8_A0(0xFF);
    GraphicLCDIntf_1_Write8_A0(0xFF);
    GraphicLCDIntf_1_Write8_A0(0xFF);
    CyDelay(10);
    //0xB0,0x01,0x00,
    GraphicLCDIntf_1_Write8_A0(0xB0); //0
    GraphicLCDIntf_1_Write8_A1(0x00);
    
    //0xB3,0x04,0x02,0x00,0x00,0x10,
    GraphicLCDIntf_1_Write8_A0(0xB3); //1
    GraphicLCDIntf_1_Write8_A1(0x02);
    GraphicLCDIntf_1_Write8_A1(0x00);
    GraphicLCDIntf_1_Write8_A1(0x00);
    GraphicLCDIntf_1_Write8_A1(0x10);
    
    //0xB4,0x01,0x11,
    GraphicLCDIntf_1_Write8_A0(0xB4); // 2
    GraphicLCDIntf_1_Write8_A1(0x11);//0X10

    // 0xC0,0x08,0x13,0x3B,0x00,0x00,0x00,0x01,0x00,0x43,
    GraphicLCDIntf_1_Write8_A0(0xC0);
    GraphicLCDIntf_1_Write8_A1(0x13);
    GraphicLCDIntf_1_Write8_A1(0x3B);//
    GraphicLCDIntf_1_Write8_A1(0x00);
    GraphicLCDIntf_1_Write8_A1(0x00);
    GraphicLCDIntf_1_Write8_A1(0x00);
    GraphicLCDIntf_1_Write8_A1(0x01);
    GraphicLCDIntf_1_Write8_A1(0x00);//NW
    GraphicLCDIntf_1_Write8_A1(0x43);

    // 0xC1,0x04,0x08,0x15,0x08,0x08,
    GraphicLCDIntf_1_Write8_A0(0xC1);
    GraphicLCDIntf_1_Write8_A1(0x08);
    GraphicLCDIntf_1_Write8_A1(0x15);//CLOCK
    GraphicLCDIntf_1_Write8_A1(0x08);
    GraphicLCDIntf_1_Write8_A1(0x08);

    // 0xC4,0x04,0x15,0x03,0x03,0x01
    GraphicLCDIntf_1_Write8_A0(0xC4);
    GraphicLCDIntf_1_Write8_A1(0x15);
    GraphicLCDIntf_1_Write8_A1(0x03);
    GraphicLCDIntf_1_Write8_A1(0x03);
    GraphicLCDIntf_1_Write8_A1(0x01);

    // 0xC6,0x01,0x02
    GraphicLCDIntf_1_Write8_A0(0xC6);
    GraphicLCDIntf_1_Write8_A1(0x02);

    // 0xC8,0x15,0x0C,0x05,0x0A,0x6B,0x04,0x06,0x15,0x10,0x00,0x31,0x10,0x15,0x06,0x64,0x0D,0x0A,0x05,0x0C,0x31,0x00
    GraphicLCDIntf_1_Write8_A0(0xC8);
    GraphicLCDIntf_1_Write8_A1(0x0c);
    GraphicLCDIntf_1_Write8_A1(0x05);
    GraphicLCDIntf_1_Write8_A1(0x0A);//0X12
    GraphicLCDIntf_1_Write8_A1(0x6B);//0x7D
    GraphicLCDIntf_1_Write8_A1(0x04);
    GraphicLCDIntf_1_Write8_A1(0x06);//0x08
    GraphicLCDIntf_1_Write8_A1(0x15);//0x0A
    GraphicLCDIntf_1_Write8_A1(0x10);
    GraphicLCDIntf_1_Write8_A1(0x00);
    GraphicLCDIntf_1_Write8_A1(0x31);//0x23
    GraphicLCDIntf_1_Write8_A1(0x10);
    GraphicLCDIntf_1_Write8_A1(0x15);//0x0A
    GraphicLCDIntf_1_Write8_A1(0x06);//0x08
    GraphicLCDIntf_1_Write8_A1(0x64);//0x74
    GraphicLCDIntf_1_Write8_A1(0x0D);//0x0B
    GraphicLCDIntf_1_Write8_A1(0x0A);//0x12
    GraphicLCDIntf_1_Write8_A1(0x05);//0x08
    GraphicLCDIntf_1_Write8_A1(0x0C);//0x06
    GraphicLCDIntf_1_Write8_A1(0x31);//0x23
    GraphicLCDIntf_1_Write8_A1(0x00);

    // 0x35,0x01,0x00
    GraphicLCDIntf_1_Write8_A0(0x35);
    GraphicLCDIntf_1_Write8_A1(0x00);
    //GraphicLCDIntf_1_Write8_A0(0x36);
    //GraphicLCDIntf_1_Write8_A1(0x00);

    // 0x0C,0x01,x066
    GraphicLCDIntf_1_Write8_A0(0x0C);
    GraphicLCDIntf_1_Write8_A1(0x66);

    //0x3A,0x01,0x55
    GraphicLCDIntf_1_Write8_A0(0x3A);
    GraphicLCDIntf_1_Write8_A1(0x55); // ARH changed to 565
    //GraphicLCDIntf_1_Write8_A1(0x66);

    // 0x44,0x02,0x00,0x01
    GraphicLCDIntf_1_Write8_A0(0x44);
    GraphicLCDIntf_1_Write8_A1(0x00);
    GraphicLCDIntf_1_Write8_A1(0x01);

    // 0xD0,0x04,0x07,0x07,0x14,0xA2,
    GraphicLCDIntf_1_Write8_A0(0xD0);
    GraphicLCDIntf_1_Write8_A1(0x07);
    GraphicLCDIntf_1_Write8_A1(0x07);//VCI1
    GraphicLCDIntf_1_Write8_A1(0x14);//VRH 0x1D
    GraphicLCDIntf_1_Write8_A1(0xA2);//BT 0x06


    // 0xD1,0x03,0x03,0x5A,0x10 
    GraphicLCDIntf_1_Write8_A0(0xD1);
    GraphicLCDIntf_1_Write8_A1(0x03);
    GraphicLCDIntf_1_Write8_A1(0x5A);//VCM  0x5A
    GraphicLCDIntf_1_Write8_A1(0x10);//VDV

    // 0xD2,0x03,0x03,0x04,0x04,
    GraphicLCDIntf_1_Write8_A0(0xD2);
    GraphicLCDIntf_1_Write8_A1(0x03);
    GraphicLCDIntf_1_Write8_A1(0x04);//0x24
    GraphicLCDIntf_1_Write8_A1(0x04);

    // 0x11,0x00,
    GraphicLCDIntf_1_Write8_A0(0x11);
    CyDelay(150);


    // 0x2A,0x04,0x00,0x00,0x01,0x3F
    GraphicLCDIntf_1_Write8_A0(0x2A);
    GraphicLCDIntf_1_Write8_A1(0x00);
    GraphicLCDIntf_1_Write8_A1(0x00);
    GraphicLCDIntf_1_Write8_A1(0x01);
    GraphicLCDIntf_1_Write8_A1(0x3F);//320


    // 0x2B,0x04,0x00,0x00,0x01,0xDF
    GraphicLCDIntf_1_Write8_A0(0x2B);
    GraphicLCDIntf_1_Write8_A1(0x00);
    GraphicLCDIntf_1_Write8_A1(0x00);
    GraphicLCDIntf_1_Write8_A1(0x01);
    GraphicLCDIntf_1_Write8_A1(0xDF);//480
    //GraphicLCDIntf_1_Write8_A0(0xB4);
    //GraphicLCDIntf_1_Write8_A1(0x00);
    
    CyDelay(100);

    // 0x29,0x00
    GraphicLCDIntf_1_Write8_A0(0x29);
    CyDelay(30);


    // 0x2C,0x00
    GraphicLCDIntf_1_Write8_A0(0x2C);
}

Here is what I get:

Screen1: Looks good, but inverted (I know how to fix)

Screen 2: Looks right, except for the blue-line at the top (who knows)

Screen 3: Seriously jacked

Earlier I told you that I much preferred to use the more compact startup code.  In order to match this, I decided to add a new code “0xDD” which means delay.  (I hope that there are no controllers out there that use 0XDD).  Here is the updated function:

static void sendStartSequence(const uint8_t *buff,uint32_t len)
{
    for(unsigned int i=0;i<len;i++)
    {
        if(buff[i] == 0xDD) // 
        {
            CyDelay(buff[i+1]);
            i=i+1;
        }
        else
        {
            GraphicLCDIntf_1_Write8_A0(buff[i]);
            i=i+1;
            unsigned int count;
            count = buff[i];
            for(unsigned int j=0;j<count;j++)
            {
                i=i+1;
                GraphicLCDIntf_1_Write8_A1(buff[i]);
            }
        }
    }
}

And when I translate the web based startup code, here is what it looks like:

static const uint8_t mcu35_init_sequence_web[]  = {
    0xFF,0x00,          // ?
    0xFF,0x00,          // ?
    0xDD,5,             // Delay 5
    0xFF,0x00,          //
    0xFF,0x00,          //
    0xFF,0x00,          //
    0xFF,0x00,          // ?
    0xDD,10,            // delay 10
    //  
    0xB0,0x01,0x00,                                     // IF Mode control
    0xB3,0x04,0x02,0x00,0x00,0x10,                      // Frame Rate Control - only 2 paramters
    0xB4,0x01,0x11,                                     // Display inversion control 
    0xC0,0x08,0x13,0x3B,0x00,0x00,0x00,0x01,0x00,0x43,  // Power Control 1
    0xC1,0x04,0x08,0x15,0x08,0x08,                      // Power Control 2
    0xC4,0x04,0x15,0x03,0x03,0x01,                      // ?
    0xC6,0x01,0x02,                                     // ?
    // ??
    0xC8,0x15,0x0C,0x05,0x0A,0x6B,0x04,0x06,0x15,0x10,0x00,0x31,0x10,0x15,0x06,0x64,0x0D,0x0A,0x05,0x0C,0x31,0x00,
    0x35,0x01,0x00,                     // Tearing Effect 
    0x0C,0x01,0x66,                     // Read pixel format?
    0x3A,0x01,0x55,                     // Pixel Format Set
    
    0x44,0x02,0x00,0x01,                // Set Tear Scanline
    0xD0,0x04,0x07,0x07,0x14,0xA2,      // NVM Write
    0xD1,0x03,0x03,0x5A,0x10,           // NVM Protection Key
    0xD2,0x03,0x03,0x04,0x04,           // NVM Status Read

    0x11,0x00,                          // Sleep Out
    0xDD,150,                           // Delay 150ms
    0x2A,0x04,0x00,0x00,0x01,0x3F,      // Column Set Address 320
    0x2B,0x04,0x00,0x00,0x01,0xDF,      // Page Set Address   480
    0xDD,100,                           // Delay 100ms
    0x29,0x00,                          // Display On
    0xDD,30,                            // delay 30ms
    0x2C,0x00                           // Memory Write
};

Notice that my comments on the commands show that there are a bunch of them I dont know what they mean.  Moreover, the MIPI spec says that all of the commands after 0xAF are reserved for the manufacturer… so I am pretty sure that they don’t do anything, or maybe should’nt be used?.  The last thing that I decide to do is edit out the stuff that does not seem to make sense.  Here is the new sequence:

static const uint8_t mcu35_init_sequence_web_edited[]  = {
    0x35,0x01,0x00,                     // Tearing Effect 
    0x3A,0x01,0x55,                     // Pixel Format Set
    0x44,0x02,0x00,0x01,                // Set Tear Scanline
    0x11,0x00,                          // Sleep Out
    0xDD,150,                           // Delay 150ms
    0x2A,0x04,0x00,0x00,0x01,0x3F,      // Column Set Address 320
    0x2B,0x04,0x00,0x00,0x01,0xDF,      // Page Set Address   480
    0xDD,100,                           // Delay 100ms
    0x29,0x00,                          // Display On
    0xDD,30,                            // delay 30ms
};

When I run this code I get the following screens:

Screen 1: Looks good, but inverted

Screen 2: Looks good (one of those codes created the blue line that is now gone)

Screen 3: Color screwed up

Conclusion

At this point I have spent a frightening amount of time figuring out how these screens work.  Although it has been a good learning experience, I have generally decided that using unknown displays from China with LCD drivers of questionable origin is not worth the pain of trying to sort out the interface.  Beyond that:

  1. emWin seems to be able to talk to the RM68140 even though it is not listed as a supported chip
  2. I have no idea what to do about screen 3.  Is it physically broken? Or do I just not know how to talk to it?
  3. There many counterfeit chips out there.. and although they may work, it probably isnt worth the effort
  4. David Prentice has added a lot of value for no personal gain by supporting the Arduino library MCUFriend_kbv

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

 

Mouser PSoC 6-WiFi-BT L8: Integrate WiFi and AWS Into the Game

Designing low-power, cloud-connected IoT devices with PSoC® 6 MCU’s and WICED® Wi-Fi/Bluetooth

Summary

In this lesson we will move the subscriber app functionality into the main GameBle project (now called GameBleAws).  In order to do this, you need to turn the subscriber into a thread, and fix it so that messages that are sent to the PADDLE topic get turned into messages that can be sent to the paddleQueue.

To implement this lesson I will follow these steps:

  1. Create a New Project starting from L6GameBle
  2. Copy over subscriber.c and wifi_config_dct.h
  3. Update the Makefile
  4. Create subscriber.h
  5. Update main.c
  6. Update subscriber.c
  7. Test

Create a New Project

Copy and paste the L6GameBle project into a new project called L8GameBleAws using Copy/Paste

Create a new Make target for the GameBleAws project

Copy subscriber.c & wifi_config_dct.h

Use copy/paste to make a copy of the subscriber application and paste it into the GameBleAws project.  Once done your folder should look like this:

Update the Makefile

NAME := App_WStudio_L8GameBleAws

$(NAME)_SOURCES := main.c \
    CapSenseThread.c \
	GameThread.c \
	cy_tft_display.c \
	GoBleThread.c \
	GoBle_db.c \
	wiced_bt_cfg.c \
	subscriber.c

$(NAME)_COMPONENTS := graphics/ugui \
                      libraries/drivers/bluetooth/low_energy \
                      protocols/AWS

WIFI_CONFIG_DCT_H := wifi_config_dct.h

$(NAME)_RESOURCES  := apps/aws/iot/rootca.cer \
                      apps/aws/iot/subscriber/client.cer \
                      apps/aws/iot/subscriber/privkey.cer
                      
# To support Low memory platforms, disabling components which are not required
GLOBAL_DEFINES += WICED_CONFIG_DISABLE_SSL_SERVER \
                  WICED_CONFIG_DISABLE_DTLS \
                  WICED_CONFIG_DISABLE_ENTERPRISE_SECURITY \
                  WICED_CONFIG_DISABLE_DES \
                  WICED_CONFIG_DISABLE_ADVANCED_SECURITY_CURVES

Create subscriber.h

In order for the main.c to know about the awsThread (which is the former application_start of subscriber.c) you should create a file called subscriber.h.  Then you should define the awsThread function to match the wiced_thread_t function prototype (just a function that takes a wiced_thread_arg and returns void).

#pragma once
extern void awsThread( wiced_thread_arg_t arg );

Update main.c

To integrate the awsThread thread into your main project you need to add the “subscriber.h” to the includes:

#include "GameThread.h"
#include "GoBleThread.h"
#include "CapSenseThread.h"
#include "subscriber.h"
#include "wiced.h"

Add a new variable to hold the awsThreadHandle.

wiced_thread_t awsThreadHandle;

Finally you should launch the AWS thread by creating it with wiced_rtos_create_thread.

void application_start( )
{
    wiced_init( );
    wiced_rtos_init_queue(&paddleQueue,"paddleQueue",sizeof(game_msg_t),10);
    wiced_rtos_create_thread(&blinkThreadHandle,7,"Blink Thread",pdlBlinkThread,500,0);
    wiced_rtos_create_thread(&capsenseThreadHandle,7,"CapSense Thread",capSenseThread,1024,0);
    wiced_rtos_create_thread(&gameThreadHandle,7,"game Thread",gameThread,4096,0);
    GoBleThread_start();
    wiced_rtos_create_thread(&awsThreadHandle,7,"AWS Thread",awsThread,4096,0);
 }

Update subscriber.c

In order for subscriber.c to be useful you need to fix the includes, send the messages from the PADDLE topic to the game thread, and turn application_start into a thread.  Start by fixing the includes (just like we did in the BLE Example in Lesson 6)

#include "SystemGlobal.h"
#include "GameThread.h"

When you get a message to the PADDLE topic, you should parse it, then send a message to the game thread to move the paddle.  You do this exactly the same way as you did in the BLE project.  Notice that I protect the game thread by making sure it send a value less than 100.

        case WICED_AWS_EVENT_PAYLOAD_RECEIVED:
        {
            uint32_t val;
            WPRINT_APP_INFO( ("[Application/AWS] Payload Received[ Topic: %.*s ]:\n", (int)data->message.topic_length, data->message.topic ) );

            if(strncmp(WICED_TOPIC,(const char *)data->message.topic,strlen(WICED_TOPIC)) == 0 && data->message.data_length < 4)
            {
                sscanf((const char *)data->message.data,"%d",(int *)&val);
                if(val>100)
                    val = 100;

                WPRINT_APP_INFO(("Val = %d\n",(int)val));
                game_msg_t msg;
                msg.evt = MSG_POSITION;
                msg.val = val;
                wiced_rtos_push_to_queue(&paddleQueue,&msg,0);
            }
        }
        break;

Test

I don’t really have a good program (like the GoBle) to test moving the paddle live.  But you can see that when the game is over, you can still move the paddle using AWS console.  In the screen shot below you can see that I moved the paddle to position 0.  And you can see that the BLE started at the same time as AWS.  Score.

Mouser PSoC 6-WiFi-BT L7 : Implement WiFi and AWS

Designing low-power, cloud-connected IoT devices with PSoC® 6 MCU’s and WICED® Wi-Fi/Bluetooth

Summary

In this lesson I am going to update one of the predefined application which we deliver as part of WICED Studio which knows how to connect to the AWS IoT Cloud to work on my network.  This will demonstrate WiFi and MQTT.  I will also fix the application so that it knows about a new MQTT Topic called “PADDLE” which will be used to send messages to move the game paddle.

To implement this project I will:

  1. Copy the subscriber from demo/aws/iot/pub_sub/subscriber
  2. Setup a “thing” on AWS and download the certificates
  3. Update the certificates in the WICED installation
  4. Update the project with my AWS configuration
  5. Test
  6. Update the project to be compatible with the Game
  7. Test

Copy Subscriber & Fix the Makefile

The first thing that I will do is copy/paste the subscriber application which can be found at demo/aws/iot/pub_sub/subscriber into my folder.  I will also rename the project to L7subscriber. Once that is done it should look like this:

Then you need to edit the Makefile to update the name of the project.

NAME := App_WStudio_L7subscriber

Go to Amazon.com and Create a “thing”

In order to connect to the AWS cloud you need to create a “thing”.  Go to the AWS console and click “Create”

We are just going to create one thing.  So, click “Create a single thing”

Give it a name, in this case Mouser.

AWS has device level security which is implemented with RSA certificates for each device.  So,  you need to let AWS create a certificate for you.

Once that is done download the device certificate and the two keys.  You need to 100% make sure you do this now, because you wont get another chance. I’m also going to activate the certificate once I have the certificate and keys.

Now attach a policy to your thing.  I already have the “ww101_policy” setup.

The policy looks like this.  Basically, things are wide open.

Update the Certificates

The last thing that you need is the most recent AWS certificate.  WICED expects that you use the RSA2048 bit key.

Now that you have the four security documents., you need to integrate them into your WICED installation.  To do this, change directories to ~/Documents/WICED-Studio-6.2/43xxx_Wi-Fi/resources/apps/aws/iot  Then copy the files, and then make symbolic links.  If you are not on a Mac or Linux then just copy the files and rename them appropriately.

Update the Project

AWS creates a virtual machine and runs an MQTT server on that machine.  You can find out the DNS name of that machine on the settings screen.  Here you can see my endpoint.  You will need to configure the WICED project to talk to your server.

You need to change the actual endpoint of your MQTT server in the AWS cloud on line 119

static wiced_aws_endpoint_info_t my_subscriber_aws_iot_endpoint = {
    .transport           = WICED_AWS_TRANSPORT_MQTT_NATIVE,
    .uri                 = "amk6m51qrxr2u-ats.iot.us-east-1.amazonaws.com",
    .peer_common_name    = NULL,
    .ip_addr             = {0},
    .port                = WICED_AWS_IOT_DEFAULT_MQTT_PORT,
    .root_ca_certificate = NULL,
    .root_ca_length      = 0,
};

In the file wifi_config_dct.h you will need to change the CLIENT_AP_SSID and CLIENT_AP_PASSPHRASE for your network

/* This is the default AP the device will connect to (as a client)*/
#define CLIENT_AP_SSID       "WW101WPA"
#define CLIENT_AP_PASSPHRASE "yourpassword"
#define CLIENT_AP_BSS_TYPE   WICED_BSS_TYPE_INFRASTRUCTURE
#define CLIENT_AP_SECURITY   WICED_SECURITY_WPA2_MIXED_PSK
#define CLIENT_AP_CHANNEL    1
#define CLIENT_AP_BAND       WICED_802_11_BAND_2_4GHZ

Test

In order to test the project you will need to create a make target and program your development kit.

Once that is done you can go to the Amazon.com test console and send MQTT messages to the correct topic.

Here you can see the project attached to my network.  And successfully received the LIGHT ON message.

Update the Project for the Game

We know that the game doesn’t care about the LIGHT topic so, let’s change it to PADDLE.

#define WICED_TOPIC                                 "PADDLE"

And let’s assume that messages to the paddle topic are ASCII numbers <100.  So, we can parse the message and print it out.  Notice that I used sscanf to parse the message and we all know that is super dangerous.

        case WICED_AWS_EVENT_PAYLOAD_RECEIVED:
        {
            char buff[10];
            uint32_t val;
            WPRINT_APP_INFO( ("[Application/AWS] Payload Received[ Topic: %.*s ]:\n", (int)data->message.topic_length, data->message.topic ) );

            if(strncmp(WICED_TOPIC,data->message.topic,strlen(WICED_TOPIC)) == 0 && data->message.data_length < 4)
            {
                sscanf(data->message.data,"%d",&val);
                WPRINT_APP_INFO(("Val = %d\n",val));
            }
        }
        break;

Test

After making those updates let’s program the whole shooting match again.  Then test by sending some messages to the PADDLE topic.

It’s good.  Things are working.

Mouser PSoC6-WiFi-BT L6 : Integrate GoBle Bluetooth into the Game

Designing low-power, cloud-connected IoT devices with PSoC® 6 MCU’s and WICED® Wi-Fi/Bluetooth

Summary

At this point we have a working BLE Remote Control and a working Game.  In this lesson I’ll merge the two projects together so that the GoBle remote control will be able to control the game paddle.  It will do this by creating messages (just like the CapSense messages) and sending them to the Game thread via the paddleQueue.  And it will send “Button0” messages when “Button A” is pressed.

To implement this lesson I will follow these steps:

  1. Copy everything into a new project
  2. Modify the Makefile
  3. Modify main.c
  4. Create a new make target
  5. Test it all to make sure things are still working
  6. Modify GoBleThread.c
  7. Program and Test

Copy L4Game into a new project L6GameBle

Use “copy” and “paste” to create a new project, when you paste give it the name “L6GameBle” (there is an error in the screenshot)

Copy the files GoBle_db.c/h, GoBleThread.c/h, and wiced_bt_cfg.c from the GoBle project into your new GameBle project.  After that is done your project should look like this:

Modify the Makefile

  1. Change the name of the App to “App_WStudio_L6GameBle”
  2. Add the new source files
  3. Add the BLE Library “libraries/drivers/bluetooth/low_energy”
NAME := App_WStudio_L6GameBle

$(NAME)_SOURCES := main.c \
    CapSenseThread.c \
	GameThread.c \
	cy_tft_display.c \
	GoBleThread.c \
	GoBle_db.c \
	wiced_bt_cfg.c

$(NAME)_COMPONENTS := graphics/ugui \
                      libraries/drivers/bluetooth/low_energy

Modify main.c

Add the include for the GeBleThread.

#include "GameThread.h"
#include "GoBleThread.h"
#include "CapSenseThread.h"
#include "wiced.h"

In the application_start function add a call GoBleThread_start() to get the GoBle thread going

void application_start( )
{
    wiced_init( );
    wiced_rtos_init_queue(&paddleQueue,"paddleQueue",sizeof(game_msg_t),10);
    wiced_rtos_create_thread(&blinkThreadHandle,7,"Blink Thread",pdlBlinkThread,500,0);
    wiced_rtos_create_thread(&capSenseThreadHandle,7,"CapSense Thread",capSenseThread,1024,0);
    wiced_rtos_create_thread(&gameThreadHandle,7,"game Thread",gameThread,4096,0);
    GoBleThread_start();
 }

Create a new make target

Test it all to make sure things are still working

Run the make target and make sure that the game still plays and that you can get remote control messages.

Modify GoBleThread.c

Add the includes for the GameThread (to get access to the game message structure) and SystemGlobal (to get access to the queue)

#include "GameThread.h"
#include "SystemGlobal.h"

I don’t need (or want) the button/slider printout information.  So I will delete the old button code from goble_event_handler. (delete this stuff)

            WPRINT_APP_INFO(("# Buttons = %d ButtonMask=%02X Slider x=%02X Slider Y=%02X Raw=",(int)numButtons,(unsigned int)buttonMask,(unsigned int)sliderX,(unsigned int)sliderY));

            for(int i=0;i<p_attr_req->data.write_req.val_len;i++)
            {
                WPRINT_APP_INFO(("%02X ",p_attr_req->data.write_req.p_val[i]));
            }
            WPRINT_APP_INFO(("\n"));

Now, update the code to send the slider and button information to the GameThread (via the paddleQueue).  You may have noticed that the slider on GoBle gives you 0->0xFF and the direction is the inverse from the game we setup.  So, I will scale the value to 100 and invert it so that the controller moves the paddle the right way on line 143.  The message format is exactly the same as what we setup for the CapSense.  This means the CapSense slider works at the same time as the GoBle controller.

   case GATT_ATTRIBUTE_REQUEST_EVT:

        p_attr_req = &p_event_data->attribute_request;
        if( p_attr_req->request_type == GATTS_REQ_TYPE_WRITE  && p_attr_req->data.handle == HDLC_GOBLE_SERIALPORTID_VALUE)
        {
            uint32_t numButtons = p_attr_req->data.write_req.p_val[3];
            uint32_t sliderY = p_attr_req->data.write_req.p_val[5+numButtons];
            uint32_t sliderX = p_attr_req->data.write_req.p_val[6+numButtons];
            uint32_t buttonMask = 0x00;
            for(int i=0;i<numButtons;i++)
            {
                buttonMask |= (1<<p_attr_req->data.write_req.p_val[5+i]);
            }

            game_msg_t msg;
            msg.evt = MSG_POSITION;
            msg.val = 100 - (100*sliderY/255);
            wiced_rtos_push_to_queue(&paddleQueue,&msg,0);
            if(buttonMask & 0x10)
            {
                msg.evt = MSG_BUTTON0;
                msg.val = 1;
                wiced_result_t result=wiced_rtos_push_to_queue(&paddleQueue,&msg,0);
            }
            status = WICED_BT_GATT_SUCCESS;
        }
        break;

Program and Test

Mouser PSoC6-WiFi-BT L5 : GoBLE – BLE Remote Control for the Game

Designing low-power, cloud-connected IoT devices with PSoC® 6 MCU’s and WICED® Wi-Fi/Bluetooth

Summary

Although I a like the video game, it is for sure missing something.  That something is IoT.  The first IoT thing that we will do to the game is to add a BLE remote control.  I wanted a pre-done remote control that could be downloaded from the App store.  After looking around a little bit I found GoBle.  I published a detailed article about GoBle here, but I’ll explain enough to make it work with the game.

Here is a screen shot of what the remote control looks like.  You can see that on the left there is a joystick, and on the right there are some buttons.  My son tells me that this is a classic layout for a remote control.

This remote control works as a “Central” meaning it knows how to connect to Peripherals.  For this lesson we will turn on the CYW4343W and make it be a Bluetooth Peripheral.

To implement this lesson I will follow these steps:

  1. Create a folder called “L5GoBle”
  2. Create a makefile called L5GoBle.mk
  3. Setup a GATT database by creating GoBle_db.h and GoBle_db.c
  4. Setup wiced_bt_cfg.c
  5. Create GoBleThread.c
  6. Create GoBle.c to startup the GoBleThread
  7. Build, Program and Test

Create a directory called “L5GoBle”

Create a makefile called L5GoBle.mk

NAME := App_WStudio_L5GoBle

$(NAME)_SOURCES    := GoBle.c \
			          GoBleThread.c \
                      GoBle_db.c \
                      wiced_bt_cfg.c

$(NAME)_INCLUDES   := .

$(NAME)_COMPONENTS += libraries/drivers/bluetooth/low_energy 

Setup a GATT database by creating GoBle_db.h and GoBle_db.c

Create a file called “GoBle_db.h”

// GoBle_db.h

#ifndef __GATT_DATABASE_H__
#define __GATT_DATABASE_H__

#include "wiced.h"

#define __UUID_GOBLE                                 0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0xb0, 0xdf, 0x00, 0x00
#define __UUID_GOBLE_SERIALPORTID                    0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0xb1, 0xdf, 0x00, 0x00

// ***** Primary Service 'Generic Attribute'
#define HDLS_GENERIC_ATTRIBUTE                       0x0001

// ***** Primary Service 'Generic Access'
#define HDLS_GENERIC_ACCESS                          0x0014
// ----- Characteristic 'Device Name'
#define HDLC_GENERIC_ACCESS_DEVICE_NAME              0x0015
#define HDLC_GENERIC_ACCESS_DEVICE_NAME_VALUE        0x0016
// ----- Characteristic 'Appearance'
#define HDLC_GENERIC_ACCESS_APPEARANCE               0x0017
#define HDLC_GENERIC_ACCESS_APPEARANCE_VALUE         0x0018

// ***** Primary Service 'GoBle'
#define HDLS_GOBLE                                   0x0028
// ----- Characteristic 'SerialPortId'
#define HDLC_GOBLE_SERIALPORTID                      0x0029
#define HDLC_GOBLE_SERIALPORTID_VALUE                0x002A
// ===== Descriptor 'Client Configuration'
#define HDLD_GOBLE_SERIALPORTID_CLIENT_CONFIGURATION 0x002B


// External definitions
extern const uint8_t  gatt_database[];
extern const uint16_t gatt_database_len;
extern uint8_t BT_LOCAL_NAME[];
extern const uint16_t BT_LOCAL_NAME_CAPACITY;

#endif /* __GATT_DATABASE_H__ */

Create a file called “GoBle.c”

/*
 * This file has been automatically generated by the WICED 20719-B1 Designer.
 * BLE device's GATT database and device configuration.
 *
 */

// GoBle_db.c

#include "GoBle_db.h"

#include "wiced.h"
#include "wiced_bt_uuid.h"
#include "wiced_bt_gatt.h"

/*************************************************************************************
** GATT server definitions
*************************************************************************************/

const uint8_t gatt_database[] = // Define GATT database
{
    /* Primary Service 'Generic Attribute' */
    PRIMARY_SERVICE_UUID16 (HDLS_GENERIC_ATTRIBUTE, UUID_SERVICE_GATT),

    /* Primary Service 'Generic Access' */
    PRIMARY_SERVICE_UUID16 (HDLS_GENERIC_ACCESS, UUID_SERVICE_GAP),

    /* Primary Service 'GoBle' */
    PRIMARY_SERVICE_UUID128 (HDLS_GOBLE, __UUID_GOBLE),

        /* Characteristic 'SerialPortId' */
        CHARACTERISTIC_UUID128_WRITABLE (HDLC_GOBLE_SERIALPORTID, HDLC_GOBLE_SERIALPORTID_VALUE,
            __UUID_GOBLE_SERIALPORTID, LEGATTDB_CHAR_PROP_READ | LEGATTDB_CHAR_PROP_WRITE_NO_RESPONSE | LEGATTDB_CHAR_PROP_WRITE | LEGATTDB_CHAR_PROP_NOTIFY,
            LEGATTDB_PERM_READABLE | LEGATTDB_PERM_WRITE_CMD | LEGATTDB_PERM_WRITE_REQ),

            /* Descriptor 'Client Characteristic Configuration' */
            CHAR_DESCRIPTOR_UUID16_WRITABLE (HDLD_GOBLE_SERIALPORTID_CLIENT_CONFIGURATION,
                UUID_DESCRIPTOR_CLIENT_CHARACTERISTIC_CONFIGURATION, LEGATTDB_PERM_READABLE | LEGATTDB_PERM_WRITE_REQ | LEGATTDB_PERM_AUTH_WRITABLE),

};

// Length of the GATT database
const uint16_t gatt_database_len = sizeof(gatt_database);

Setup wiced_bt_cfg.c

The WICED Bluetooth Configuration file is called “wiced_bt_config.c”.  The only change I made from the default is the timeout value of the advertising.  You can copy this directly into your file.

/*
* This file has been automatically generated by the WICED 20719-B1 Designer.
* Device Configuration.
*
*/
/** wiced_bt_cfg.c
*
* Runtime Bluetooth stack configuration parameters
*
*/
#include "wiced_bt_dev.h"
#include "wiced_bt_ble.h"
#include "wiced_bt_uuid.h"
#include "wiced_bt_gatt.h"
#include "wiced_bt_avrc.h"
#include "wiced_bt_cfg.h"
/* Null-Terminated Local Device Name */
uint8_t BT_LOCAL_NAME[] = { 'G','o','B','l','e','T','e','s','t','\0' };
const uint16_t BT_LOCAL_NAME_CAPACITY = sizeof(BT_LOCAL_NAME);
/*******************************************************************
* wiced_bt core stack configuration
******************************************************************/
const wiced_bt_cfg_settings_t wiced_bt_cfg_settings =
{
.device_name =                          (uint8_t*)BT_LOCAL_NAME,                                    /**< Local device name (NULL terminated) */
.device_class =                         {0x00, 0x00, 0x00},                                         /**< Local device class */
.security_requirement_mask =            BTM_SEC_NONE,                                               /**< Security requirements mask (BTM_SEC_NONE, or combination of BTM_SEC_IN_AUTHENTICATE, BTM_SEC_OUT_AUTHENTICATE, BTM_SEC_ENCRYPT */
.max_simultaneous_links =               1,                                                          /**< Maximum number of simultaneous links to different devices */
/* BR/EDR Scan Configuration */
.br_edr_scan_cfg = {
.inquiry_scan_type =                BTM_SCAN_TYPE_STANDARD,                                     /**< Inquiry Scan Type (BTM_SCAN_TYPE_STANDARD or BTM_SCAN_TYPE_INTERLACED) */
.inquiry_scan_interval =            0x0000,                                                     /**< Inquiry Scan Interval (0 to use default) */
.inquiry_scan_window =              0x0000,                                                     /**< Inquiry Scan Window (0 to use default) */
.page_scan_type =                   BTM_SCAN_TYPE_STANDARD,                                     /**< Page Scan Type (BTM_SCAN_TYPE_STANDARD or BTM_SCAN_TYPE_INTERLACED) */
.page_scan_interval =               0x0000,                                                     /**< Page Scan Interval (0 to use default) */
.page_scan_window =                 0x0000,                                                     /**< Page Scan Window (0 to use default) */
},
/* BLE Scan Settings */
.ble_scan_cfg = {
.scan_mode =                        BTM_BLE_SCAN_MODE_PASSIVE,                                  /**< BLE Scan Mode (BTM_BLE_SCAN_MODE_PASSIVE or BTM_BLE_SCAN_MODE_ACTIVE) */
/* Advertisement Scan Configuration */
.high_duty_scan_interval =          WICED_BT_CFG_DEFAULT_HIGH_DUTY_SCAN_INTERVAL,               /**< High Duty Scan Interval */
.high_duty_scan_window =            WICED_BT_CFG_DEFAULT_HIGH_DUTY_SCAN_WINDOW,                 /**< High Duty Scan Window */
.high_duty_scan_duration =          5,                                                          /**< High Duty Scan Duration in seconds (0 for infinite) */
.low_duty_scan_interval =           WICED_BT_CFG_DEFAULT_LOW_DUTY_SCAN_INTERVAL,                /**< Low Duty Scan Interval */
.low_duty_scan_window =             WICED_BT_CFG_DEFAULT_LOW_DUTY_SCAN_WINDOW,                  /**< Low Duty Scan Window */
.low_duty_scan_duration =           5,                                                          /**< Low Duty Scan Duration in seconds (0 for infinite) */
/* Connection Scan Configuration */
.high_duty_conn_scan_interval =     WICED_BT_CFG_DEFAULT_HIGH_DUTY_CONN_SCAN_INTERVAL,          /**< High Duty Connection Cycle Connection Scan Interval */
.high_duty_conn_scan_window =       WICED_BT_CFG_DEFAULT_HIGH_DUTY_CONN_SCAN_WINDOW,            /**< High Duty Connection Cycle Connection Scan Window */
.high_duty_conn_duration =          30,                                                         /**< High Duty Connection Cycle Connection Duration in seconds (0 for infinite) */
.low_duty_conn_scan_interval =      WICED_BT_CFG_DEFAULT_LOW_DUTY_CONN_SCAN_INTERVAL,           /**< Low Duty Connection Cycle Connection Scan Interval */
.low_duty_conn_scan_window =        WICED_BT_CFG_DEFAULT_LOW_DUTY_CONN_SCAN_WINDOW,             /**< Low Duty Connection Cycle Connection Scan Window */
.low_duty_conn_duration =           30,                                                         /**< Low Duty Connection Cycle Connection Duration in seconds (0 for infinite) */
/* Connection Configuration */
.conn_min_interval =                WICED_BT_CFG_DEFAULT_CONN_MIN_INTERVAL,                     /**< Minimum Connection Interval */
.conn_max_interval =                WICED_BT_CFG_DEFAULT_CONN_MAX_INTERVAL,                     /**< Maximum Connection Interval */
.conn_latency =                     WICED_BT_CFG_DEFAULT_CONN_LATENCY,                          /**< Connection Latency */
.conn_supervision_timeout =         WICED_BT_CFG_DEFAULT_CONN_SUPERVISION_TIMEOUT,              /**< Connection Link Supervision Timeout */
},
/* BLE Advertisement Settings */
.ble_advert_cfg = {
.channel_map =                      BTM_BLE_ADVERT_CHNL_37 |                                    /**< Advertising Channel Map (mask of BTM_BLE_ADVERT_CHNL_37, BTM_BLE_ADVERT_CHNL_38, BTM_BLE_ADVERT_CHNL_39) */
BTM_BLE_ADVERT_CHNL_38 |
BTM_BLE_ADVERT_CHNL_39,
.high_duty_min_interval =           WICED_BT_CFG_DEFAULT_HIGH_DUTY_ADV_MIN_INTERVAL,            /**< High Duty Undirected Connectable Minimum Advertising Interval */
.high_duty_max_interval =           WICED_BT_CFG_DEFAULT_HIGH_DUTY_ADV_MAX_INTERVAL,            /**< High Duty Undirected Connectable Maximum Advertising Interval */
.high_duty_duration =               0,                                                         /**< High Duty Undirected Connectable Advertising Duration in seconds (0 for infinite) */
.low_duty_min_interval =            WICED_BT_CFG_DEFAULT_LOW_DUTY_ADV_MIN_INTERVAL,             /**< Low Duty Undirected Connectable Minimum Advertising Interval */
.low_duty_max_interval =            WICED_BT_CFG_DEFAULT_LOW_DUTY_ADV_MAX_INTERVAL,             /**< Low Duty Undirected Connectable Maximum Advertising Interval */
.low_duty_duration =                60,                                                         /**< Low Duty Undirected Connectable Advertising Duration in seconds (0 for infinite) */
.high_duty_directed_min_interval =  WICED_BT_CFG_DEFAULT_HIGH_DUTY_DIRECTED_ADV_MIN_INTERVAL,   /**< High Duty Directed Minimum Advertising Interval */
.high_duty_directed_max_interval =  WICED_BT_CFG_DEFAULT_HIGH_DUTY_DIRECTED_ADV_MAX_INTERVAL,   /**< High Duty Directed Maximum Advertising Interval */
.low_duty_directed_min_interval =   WICED_BT_CFG_DEFAULT_LOW_DUTY_DIRECTED_ADV_MIN_INTERVAL,    /**< Low Duty Directed Minimum Advertising Interval */
.low_duty_directed_max_interval =   WICED_BT_CFG_DEFAULT_LOW_DUTY_DIRECTED_ADV_MAX_INTERVAL,    /**< Low Duty Directed Maximum Advertising Interval */
.low_duty_directed_duration =       30,                                                         /**< Low Duty Directed Advertising Duration in seconds (0 for infinite) */
.high_duty_nonconn_min_interval =   WICED_BT_CFG_DEFAULT_HIGH_DUTY_NONCONN_ADV_MIN_INTERVAL,    /**< High Duty Non-Connectable Minimum Advertising Interval */
.high_duty_nonconn_max_interval =   WICED_BT_CFG_DEFAULT_HIGH_DUTY_NONCONN_ADV_MAX_INTERVAL,    /**< High Duty Non-Connectable Maximum Advertising Interval */
.high_duty_nonconn_duration =       30,                                                         /**< High Duty Non-Connectable Advertising Duration in seconds (0 for infinite) */
.low_duty_nonconn_min_interval =    WICED_BT_CFG_DEFAULT_LOW_DUTY_NONCONN_ADV_MIN_INTERVAL,     /**< Low Duty Non-Connectable Minimum Advertising Interval */
.low_duty_nonconn_max_interval =    WICED_BT_CFG_DEFAULT_LOW_DUTY_NONCONN_ADV_MAX_INTERVAL,     /**< Low Duty Non-Connectable Maximum Advertising Interval */
.low_duty_nonconn_duration =        0,                                                          /**< Low Duty Non-Connectable Advertising Duration in seconds (0 for infinite) */
},
/* GATT Configuration */
.gatt_cfg = {
.appearance =                       0x0000,                                                     /**< GATT Appearance */
.client_max_links =                 1,                                                          /**< Client Config: Maximum number of servers that local client can connect to */
.server_max_links =                 1,                                                          /**< Server Config: Maximum number of remote client connections allowed by local server */
.max_attr_len =                     512,                                                        /**< Maximum attribute length; wiced_bt_cfg must have a corresponding buffer pool that can hold this length */
.max_mtu_size =                     515,                                                        /**< Maximum MTU size for GATT connections, should be between 23 and (max_attr_len + 5) */
},
/* RFCOMM Configuration */
.rfcomm_cfg = {
.max_links =                        0,                                                          /**< Maximum number of simultaneous connected remote devices */
.max_ports =                        0,                                                          /**< Maximum Number of simultaneous RFCOMM ports */
},
/* Application-Managed L2CAP Protocol Configuration */
.l2cap_application = {
.max_links =                        0,                                                          /**< Maximum Number of Application-Managed L2CAP Links (BR/EDR and BLE) */
.max_psm =                          0,                                                          /**< Maximum Number of Application-Managed BR/EDR PSMs */
.max_channels =                     0,                                                          /**< Maximum Number of Application-Managed BR/EDR Channels */
.max_le_psm =                       0,                                                          /**< Maximum Number of Application-Managed LE PSMs */
.max_le_channels =                  0,                                                          /**< Maximum Number of Application-Managed LE Channels */
.max_le_l2cap_fixed_channels =      0,                                                          /**< Maximum Number of Application-Managed LE L2CAP Fixed Channnels supported (in addition to mandatory channels 4, 5, and 6 */
},
/* Audio/Video Distribution Configuration */
.avdt_cfg = {
.max_links =                        0,                                                          /**< Maximum Number of simultaneous Audio/Video links */
.max_seps =                         0,                                                          /**< Maximum Number of stream end points */
},
/* AVRC Configuration */
.avrc_cfg = {
.roles =                            0,                                                          /**< Local Roles supported (AVRC_CONN_INITIATOR or AVRC_CONN_ACCEPTOR) */
.max_links =                        0,                                                          /**< Maximum simultaneous Remote Control links */
},
/* LE Address Resolution Database Settings */
.addr_resolution_db_size =              10,                                                         /**< LE Address Resolution Database Size - Effective only for pre-4.2 controller */
.max_number_of_buffer_pools =           4,                                                          /**< Maximum number of buffer pools in p_btm_cfg_buf_pools and by wiced_create_pool */
.rpa_refresh_timeout =                  0,         /**< Interval of random address refreshing - secs */
};
/*******************************************************************
* wiced_bt_stack buffer pool configuration
*
* Configure buffer pools used by the stack
*
* Pools must be ordered in increasing buf_size.
* If a pools runs out of buffers, the next pool will be used.
******************************************************************/
const wiced_bt_cfg_buf_pool_t wiced_bt_cfg_buf_pools[WICED_BT_CFG_NUM_BUF_POOLS] =
{
/*  { buf_size, buf_count, }, */
{ 64,       12,        }, /* Small Buffer Pool */
{ 360,      4,         }, /* Medium Buffer Pool (used for HCI & RFCOMM control messages, min recommended size is 360) */
{ 512,      4,         }, /* Large Buffer Pool  (used for HCI ACL messages) */
{ 1024,     2,         }, /* Extra Large Buffer Pool (used for AVDT media packets and miscellaneous; if not needed, set buf_count to 0) */
};

Create GoBleThread.c

In  order for the GoBleThread to work you need:

  1. The includes for the GATT Database and the WICED bluetooth stack.
  2. External References to the GATT Database.
  3. A few function prototypes.
  4. A function called “GoBleThread_start” to startup the Bluetooth stack and get things going. This includes providing Bluetooth management and GATT event handler functions.
#include "GoBle_db.h"
#include "wiced.h"
#include "wiced_bt_ble.h"
#include "wiced_bt_gatt.h"
#include "wiced_bt_stack.h"
/*******************************************************************
* Variable Definitions
******************************************************************/
extern const wiced_bt_cfg_settings_t wiced_bt_cfg_settings;
extern const wiced_bt_cfg_buf_pool_t wiced_bt_cfg_buf_pools[WICED_BT_CFG_NUM_BUF_POOLS];
/*******************************************************************
* Function Prototypes
******************************************************************/
static wiced_bt_dev_status_t  goble_management_callback    ( wiced_bt_management_evt_t event, wiced_bt_management_evt_data_t *p_event_data );
static void                   goble_set_advertisement_data ( void );
static wiced_bt_gatt_status_t goble_event_handler          ( wiced_bt_gatt_evt_t  event, wiced_bt_gatt_event_data_t *p_event_data );
/*******************************************************************
* Function Definitions
******************************************************************/
void GoBleThread_start(void)
{
wiced_bt_stack_init(goble_management_callback, &wiced_bt_cfg_settings, wiced_bt_cfg_buf_pools);
}

Create a function to setup the advertising data.  The GoBle iOS App looks for Peripherals that are advertising the UUID of the GoBLE Service.

/* Set Advertisement Data */
void goble_set_advertisement_data( void )
{
wiced_bt_ble_advert_elem_t adv_elem[3] = { 0 };
uint8_t adv_flag = BTM_BLE_GENERAL_DISCOVERABLE_FLAG | BTM_BLE_BREDR_NOT_SUPPORTED;
uint8_t num_elem = 0; 
/* Advertisement Element for Flags */
adv_elem[num_elem].advert_type = BTM_BLE_ADVERT_TYPE_FLAG;
adv_elem[num_elem].len = sizeof(uint8_t);
adv_elem[num_elem].p_data = &adv_flag;
num_elem++;
uint8_t gobleuuid[] = {__UUID_GOBLE};
/* Advertisement Element for Name */
adv_elem[num_elem].advert_type = BTM_BLE_ADVERT_TYPE_128SRV_COMPLETE;
adv_elem[num_elem].len = 16;
adv_elem[num_elem].p_data = gobleuuid;
num_elem++;
/* Set Raw Advertisement Data */
wiced_bt_ble_set_raw_advertisement_data(num_elem, adv_elem);
}

The Bluetooth Management Event Handler needs to take actions when the stack turns on, or one of the pairing events occur.

/* Bluetooth Management Event Handler */
wiced_bt_dev_status_t goble_management_callback( wiced_bt_management_evt_t event, wiced_bt_management_evt_data_t *p_event_data )
{
wiced_bt_dev_status_t status = WICED_BT_SUCCESS;
switch (event)
{
case BTM_ENABLED_EVT:
goble_set_advertisement_data();
wiced_bt_gatt_register( goble_event_handler );
wiced_bt_gatt_db_init( gatt_database, gatt_database_len );
wiced_bt_start_advertisements(BTM_BLE_ADVERT_UNDIRECTED_HIGH, 0, NULL);
break;
case BTM_SECURITY_REQUEST_EVT:
wiced_bt_ble_security_grant(p_event_data->security_request.bd_addr, WICED_BT_SUCCESS);
break;
case BTM_PAIRING_IO_CAPABILITIES_BLE_REQUEST_EVT:
p_event_data->pairing_io_capabilities_ble_request.local_io_cap = BTM_IO_CAPABILITIES_NONE;
p_event_data->pairing_io_capabilities_ble_request.oob_data = BTM_OOB_NONE;
p_event_data->pairing_io_capabilities_ble_request.auth_req = BTM_LE_AUTH_REQ_NO_BOND;
break;
case BTM_USER_CONFIRMATION_REQUEST_EVT: // Just confirm
wiced_bt_dev_confirm_req_reply( WICED_BT_SUCCESS , p_event_data->user_confirmation_request.bd_addr);
break;
case BTM_PAIRED_DEVICE_LINK_KEYS_REQUEST_EVT:
WPRINT_APP_INFO(("Paired linke keys\n"));
status = WICED_BT_ERROR;
break;
case BTM_LOCAL_IDENTITY_KEYS_REQUEST_EVT:
case BTM_PAIRING_COMPLETE_EVT:
case BTM_ENCRYPTION_STATUS_EVT:
case BTM_BLE_ADVERT_STATE_CHANGED_EVT:
case BTM_LPM_STATE_LOW_POWER:
break;
default:
WPRINT_APP_INFO(("Unhandled Bluetooth Management Event: 0x%x (%d)\n", event, event));
break;
}
return status;
}

The GATT Event Handler is called

  1. When a connection is made or terminated
  2. When the GoBle app writes to your GATT database.  When that happens we will just printout the value that was written
/* GATT Event Handler */
wiced_bt_gatt_status_t goble_event_handler( wiced_bt_gatt_evt_t event, wiced_bt_gatt_event_data_t *p_event_data )
{
wiced_bt_gatt_status_t status = WICED_BT_GATT_ERROR;
wiced_bt_gatt_attribute_request_t *p_attr_req = NULL;
switch ( event )
{
case GATT_CONNECTION_STATUS_EVT:
if(!p_event_data->connection_status.connected)
{
WPRINT_APP_INFO(("Disconnected\n"));
wiced_bt_start_advertisements(BTM_BLE_ADVERT_UNDIRECTED_HIGH, 0, NULL);
}
else
WPRINT_APP_INFO(("Connected\n"));
break;
case GATT_ATTRIBUTE_REQUEST_EVT:
p_attr_req = &p_event_data->attribute_request;
if( p_attr_req->request_type == GATTS_REQ_TYPE_WRITE  && p_attr_req->data.handle == HDLC_GOBLE_SERIALPORTID_VALUE)
{
uint32_t numButtons = p_attr_req->data.write_req.p_val[3];
uint32_t sliderY = p_attr_req->data.write_req.p_val[5+numButtons];
uint32_t sliderX = p_attr_req->data.write_req.p_val[6+numButtons];
uint32_t buttonMask = 0x00;
for(int i=0;i<numButtons;i++)
{
buttonMask |= (1<<p_attr_req->data.write_req.p_val[5+i]);
}
WPRINT_APP_INFO(("# Buttons = %d ButtonMask=%02X Slider x=%02X Slider Y=%02X Raw=",(int)numButtons,(unsigned int)buttonMask,(unsigned int)sliderX,(unsigned int)sliderY));
for(int i=0;i<p_attr_req->data.write_req.val_len;i++)
{
WPRINT_APP_INFO(("%02X ",p_attr_req->data.write_req.p_val[i]));
}
WPRINT_APP_INFO(("\n"));
status = WICED_BT_GATT_SUCCESS;
}
break;
default:
status = WICED_BT_GATT_SUCCESS;
break;
}
return status;
}

Create GoBleThread.h

#pragma once
extern void GoBleThread_start(void);

Create GoBle.c to startup the GoBleThread

#include "GoBleThread.h"
#include "wiced.h"
/*******************************************************************
* Function Definitions
******************************************************************/
void application_start(void)
{
wiced_init();
GoBleThread_start();
}

Build, Program and Test

Mouser PSoC6-WiFi-BT L4 : The Video Game

Designing low-power, cloud-connected IoT devices with PSoC® 6 MCU’s and WICED® Wi-Fi/Bluetooth

Summary

In this lesson I’ll finish the video game thread by adding the graphics etc. to play the game.  In addition I’ll fix up the CapSense thread so that it is connected to the game via an RTOS queue.

There are three main things going on in this game.

  1. A state machine for the game screen (Splash, Start, Running, Over)
  2. A 20ms timer that updates the screen while the game is running (moves the Paddle and the Ball)
  3. A GUI queue where the rest of the system – CapSense,  Bluetooth and WiFi – can send Button and Paddle messages.

To implement this project I will:

  1. Setup the project and makefile by copying L3CapSenseTft
  2. Update gamethread.h to define the GUI queue messages
  3. Fix main.c to create the queue
  4. Create SystemGlobal.h to give the rest of the files access to the gui queue
  5. Updating the CapSenseThread to send GUI messages
  6. Update the includes in GameThread.c
  7. Add some #define macros to define game parameters
  8. Add a State Machine for the game & define some paddle movement methods
  9. Make forward declarations for the thread functions
  10. Create some variables to maintain game state
  11. Add display functions for the score and the speed
  12. Add functions to start and end the game
  13. Add helper functions to calculate the top and bottom of the paddle
  14. Add a function to update and draw the paddle
  15. Add a function to update and draw the ball
  16. Add a function for the game timer to call
  17. Update the main game thread

Setup the project and makefile by copying L3CapSenseTft

Use copy/paste to copy the L3CapSenseTft project to a new folder name L4Game.   Change the name of the makefile to be L4Game.mk.

Edit the makefile and change the name of the application.

NAME := App_WStudio_L4Game
$(NAME)_SOURCES := main.c \
CapSenseThread.c \
GameThread.c \
cy_tft_display.c
$(NAME)_COMPONENTS := graphics/ugui

Create a make target for this project

Update GameThread.h to Define the GUI Messages

All of the threads in the system (CapSense, Bluetooth, and WiFi) will control the paddle and the button by sending messages to an RTOS queue.  In gameThread.h we will add a definition of that message.  The message is just a structure with two values – which GUI element and what value to send.

#pragma once
#include "wiced.h"
typedef enum {
MSG_POSITION,
MSG_BUTTON0,
MSG_BUTTON1,
} game_evt_t;
typedef struct {
game_evt_t evt;
uint32_t val;
} game_msg_t;
void gameThread(wiced_thread_arg_t arg);

Fix main.c to Create the Queue

I typically believe that the RTOS primitives should be owned by the main.c.  To do this edit main.c and fix the includes.

#include "GameThread.h"
#include "wiced.h"
#include "CapSenseThread.h"
#include "SystemGlobal.h"

Then define the queue variable “paddleQueue” which I should have names “guiQueue” but it is way to late to fix it now — oh well.

/******************************************************
*               Variable Definitions
******************************************************/
wiced_thread_t blinkThreadHandle;
wiced_thread_t capSenseThreadHandle;
wiced_thread_t gameThreadHandle;
wiced_queue_t paddleQueue;

Create the queue

void application_start( )
{
wiced_init( );
wiced_rtos_init_queue(&paddleQueue,"paddleQueue",sizeof(game_msg_t),10);
wiced_rtos_create_thread(&blinkThreadHandle,7,"Blink Thread",pdlBlinkThread,500,0);
wiced_rtos_create_thread(&capSenseThreadHandle,7,"CapSense Thread",capSenseThread,1024,0);
wiced_rtos_create_thread(&gameThreadHandle,7,"game Thread",gameThread,4096,0);
}

Create SystemGlobal.h

Each of the threads in the system need to have access to the paddleQueue.  In order to do that create a file called SystemGlobal.h and extern the variable to give them that access.

#pragma once
extern wiced_queue_t paddleQueue;

Updating the CapSenseThread to send GUI messages

Remember that when we setup the CapSenseThread originally, it just printed out the values.  Let’s fix it so send messages.  So, edit CapSenseThread.c.

  1. Add a message variable (line 8)
  2. Fix the button0 and button 1 to make and send RTOS messages (lines 20/21 and 26/27)
  3. Fix the slider to send the position (lines 33-35)
#include "wiced.h"
#include "GameThread.h"
#include "SystemGlobal.h"
void capSenseThread(wiced_thread_arg_t arg)
{
game_msg_t msg;
CapSense_Start();
CapSense_ScanAllWidgets();
while(1)
{
if(!CapSense_IsBusy())
{
CapSense_ProcessAllWidgets();
if(CapSense_IsWidgetActive(CapSense_BUTTON0_WDGT_ID))
{
msg.evt = MSG_BUTTON0;
wiced_rtos_push_to_queue(&paddleQueue,&msg,0);
}
if(CapSense_IsWidgetActive(CapSense_BUTTON1_WDGT_ID))
{
msg.evt = MSG_BUTTON1;
wiced_rtos_push_to_queue(&paddleQueue,&msg,0);
}
uint32_t val = CapSense_GetCentroidPos(CapSense_LINEARSLIDER0_WDGT_ID);
if(val < 0xFFFF)
{
msg.evt = MSG_POSITION;
msg.val = val;
wiced_rtos_push_to_queue(&paddleQueue,&msg,0);
}
CapSense_ScanAllWidgets();
}
wiced_rtos_delay_milliseconds(25); // Poll every 25ms (actual scan time ~8ms)
}
}

Update the includes in GameThread.c

Now let’s fix GameThread.c.  Start by editing the includes to add a new file called “SystemGlobal.h” which contains the global variable for the GUI queue.

#include "GameThread.h"
#include "cy_tft_display.h"
#include "SystemGlobal.h"
#include "ugui.h"

Add some #define macros in GameThread.c

There are a number of constants which I use in the game.  In this section I use #define macros to define them.

#define UPDATE_SCREEN_TIME (20) // Update the screen every 20ms
#define SPEED (2)
#define SCREEN_X (320)
#define SCREEN_Y (240)
#define TOP_FIELD (21)
#define PD_WIDTH (10)
#define PD_LEN (70)
#define DOTS (3)
#define PADDLE0_COLOR (C_BLUE)
#define BALL_COLOR (C_GREEN)
#define BALL_SIZE (10)

Add a State Machine for the Game & Define Paddle Movement

Open up GameThread.c – all of the game control functions will go there.

There will be four screens in the game.  A splash screen to display Cypress and Mouser, a Ready Player 1 Screen, the actual game screen and a game over screen.

In addition the paddle can move a little bit at a time (increment) or jump directly to the position (absolute)

// States of the game
typedef enum {
GS_SPLASH,
GS_START,
GS_RUNNING,
GS_OVER
} game_state_t;
// Methods to move the paddle
typedef enum {
PADDLE_INCREMENT,
PADDLE_ABSOLUTE
} paddle_update_t;

Fix the gameState statemachine

In the splash screen I need to set the state machine to GS_SPLASH

static void displaySplashScreen()
{
gameState = GS_SPLASH;
UG_FontSelect( &FONT_22X36 );
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/5,22,36,"Cypress");
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/5*2,22,36,"Mouser");
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/5*3,22,36,"PSoC 6");
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/5*4,22,36,"WICED 4343");
wiced_rtos_delay_milliseconds(2000);
}

In the start screen I need to set the state machine to GS_START

// Display the Start Screen
static void  displayStartScreen()
{
gameState = GS_START;
UG_FillScreen( C_BLACK );
UG_FontSelect( &FONT_22X36 );
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/2 -2 - 18 ,22,36,"Ready");
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/2 + 2 + 18 ,22,36,"Player 1");
displayStartButton();
}

Make Forward Declarations for Functions

You should define the functions in advance of using them

/******************************************************
*               Static Function Declarations
******************************************************/
static void UG_PutStringCenter(uint32_t x, uint32_t y, uint32_t fontx, uint32_t fonty,char *string);
static void displaySplashScreen();
static void displayStartButton();
static void  displayStartScreen();
static void displayScore();
static void displaySpeed();
static void endGame();
static inline uint32_t calcPaddleTop();
static inline uint32_t calcPaddleBottom();
static void updatePaddle(paddle_update_t type);
static void updateBall();
static void updateScreen(void *arg);

Create some variables to maintain game state

The updateScreenTimer is used while the game is running to call the updateScreen every 20ms.  The rest of the variables are self explanatory.

/******************************************************
*               Variable Definitions
******************************************************/
static UG_GUI   gui;
static wiced_timer_t updateScreenTimer;
static uint32_t gameScore;
static game_state_t gameState;
// position of the paddle
static uint32_t paddle0_desire_pos=0;
static uint32_t paddle0_cur_pos=0;
// Position, direction and speed of the ball
static uint32_t ballx,bally;
static int32_t ballXdir, ballYdir;
static uint32_t ballSpeed;

Add Display Functions for the Score & Speed

These two functions print the speed and score at the top of the screen.

// This function displays the score
static void displayScore()
{
char buff[10];
sprintf(buff,"%2X",(unsigned int)gameScore);
UG_FontSelect(&FONT_12X20);
UG_PutString( 75, 0, buff);
}
// This function displays the speed
static void displaySpeed()
{
char buff[10];
sprintf(buff,"%2X",(unsigned int)ballSpeed-1);
UG_FontSelect(&FONT_12X20);
UG_PutString( 275, 0, buff);
}

Add Function to Start the Game

When the game needs to start you:

  1. Reset the score
  2. Set the paddle position
  3. Move the ball to the middle of the paddle
  4. Set the ball to move to the right and down
  5. Clear the screen, display score and speed
  6. Start the game running
// This function initializes everything and starts a new game
static void startGame()
{
gameScore = 0;
paddle0_desire_pos = 50; // start the game with the paddle moving
paddle0_cur_pos = 0;
ballx = PD_WIDTH ;                   // start the ball on the paddle on the right of the screen
bally  = calcPaddleTop() + PD_LEN/2; // start the ball in the middle of the paddle
ballSpeed = SPEED;
ballXdir = ballSpeed;
ballYdir = ballSpeed;
UG_FillScreen( C_BLACK );  // clear screen
UG_FontSelect(&FONT_12X20);
UG_PutString( 0, 0,  "Score:");
displayScore();
UG_PutString(200,0,"Speed:");
displaySpeed();
UG_DrawLine(0,20,SCREEN_X,20,C_RED); // red line under text to represent top of play screen
gameState = GS_RUNNING;
wiced_rtos_start_timer(&updateScreenTimer); // Timer to update screen
}

Add Function to End the Game

When the game is over you should:

  1. Move the game state to over
  2. Stop the timer
  3. Display game over
  4. Display press button 0 to start
// Stop the game
static void endGame()
{
gameState = GS_OVER;
wiced_rtos_stop_timer(&updateScreenTimer);
UG_FontSelect( &FONT_22X36 );
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/2,22,36,"Game Over");
displayStartButton();
}

Add Helper Functions to Calculate Paddle Top & Bottom

There are two places where you need to know the position of the Paddle.  Specifically:

  1. To draw the paddle
  2. To figure out if the ball hit the paddle or not.

These two functions calculate the pixel position of the top and bottom of the paddle based on it current position

// Figure out the y position of the top of the paddle
static inline uint32_t calcPaddleTop()
{
return (paddle0_cur_pos)*DOTS+TOP_FIELD;
}
// Figure out the y position of the bottom of the paddle
static inline uint32_t calcPaddleBottom()
{
return (paddle0_cur_pos)*DOTS+PD_LEN+TOP_FIELD;
}

Add a Function to Update & Draw the Paddle

While the game is running you need the paddle to move.  There are two methods:

  1. Absolute just moves the current position immediately to the desired position.
  2. Incremental, which moves the paddle a little bit towards the desired position.
// Move the paddle either to : PADDLE_INCREMENT the next location or PADDLE_ABSOLUTE - final location
static void updatePaddle(paddle_update_t type)
{
// If the paddle is where it is supposed to be then just return
if(paddle0_cur_pos == paddle0_desire_pos)
return;
// erase the current paddle
UG_FillFrame(0,calcPaddleTop(),PD_WIDTH,calcPaddleBottom(),C_BLACK);
switch (type)
{
case PADDLE_INCREMENT:
if(paddle0_cur_pos < paddle0_desire_pos)
paddle0_cur_pos += SPEED;
else
paddle0_cur_pos -= SPEED;
// If the paddle is within one move of the final spot, put it there
if(abs((int)paddle0_cur_pos-(int)paddle0_desire_pos) < SPEED)
paddle0_cur_pos = paddle0_desire_pos;
break;
case PADDLE_ABSOLUTE:
paddle0_cur_pos = paddle0_desire_pos;
break;
}
// draw the paddle
UG_FillFrame(0,calcPaddleTop(),PD_WIDTH,calcPaddleBottom(),PADDLE0_COLOR);
}

Add a function to update and draw the ball

You need a function to:

  1. Move the ball
  2. Figure out if it hit the right/left/top/bottom of the screen and do the right thing.

When the ball hits one of those surfaces it needs to change direction to either plus or minus.

Every time it hits the paddle the score should increase and possibly speed up.

If it misses the paddle the game is over.

// Move the ball to the next location
static void updateBall()
{
static const uint32_t BallFudgeFactor=3;
UG_DrawCircle(ballx,bally,BALL_SIZE,C_BLACK);
ballx += ballXdir;
bally += ballYdir;
// Check to see if the ball hit the far right side
if(ballx > SCREEN_X - BALL_SIZE)
{
ballx = SCREEN_X - BALL_SIZE;
ballXdir = -ballSpeed;
}
// check to see if the ball hit the far left side... or the paddle
if(ballx < (BALL_SIZE + PD_WIDTH + BallFudgeFactor))
{
// See if the ball missed the paddle
if(bally + BALL_SIZE < calcPaddleTop() || bally - BALL_SIZE > calcPaddleBottom())
{
endGame();
//WPRINT_APP_INFO(("Missed Paddle\r\n"));
}
gameScore = gameScore + 1;
displayScore();
if(gameScore % 3 == 0) // Speed up every three hits
{
ballSpeed +=1;
displaySpeed();
}
ballx = BALL_SIZE + PD_WIDTH + BallFudgeFactor;
ballXdir = +ballSpeed;
}
// Check to see if the ball hit the top or bottom
if(bally > SCREEN_Y - BALL_SIZE) // bottom
{
bally = SCREEN_Y - BALL_SIZE;
ballYdir = -ballSpeed;
}
if(bally < TOP_FIELD+BALL_SIZE) // top
{
bally = BALL_SIZE+TOP_FIELD;
ballYdir = +ballSpeed;
}
UG_DrawCircle(ballx,bally,BALL_SIZE,BALL_COLOR);
}

Create a Function for the Game Timer

An RTOS timer runs every 20ms.  That timer needs a function to move the paddle and move the ball.

// This function is called every UPADTE_SCREEN_TIME milliseconds by the updateScreenTimer
static void updateScreen(void *arg)
{
updatePaddle(PADDLE_INCREMENT);
updateBall();
}

Update the Main Game Thread

The main game thread needs to get messages out of the queue and then do the right thing based on the game state.

// Main game thread
void gameThread(wiced_thread_arg_t arg)
{
game_msg_t msg;
Cy_TFT_Init();                                             // Init the TFT
UG_Init( &gui, Cy_TFT_displayDriver, SCREEN_X, SCREEN_Y ); // Connect the driver
UG_FillScreen( C_BLACK );   // Clear the screen
UG_SetBackcolor( C_BLACK );
UG_SetForecolor( C_WHITE );
wiced_rtos_init_timer(&updateScreenTimer,UPDATE_SCREEN_TIME,updateScreen,0);
displaySplashScreen();
displayStartScreen();
while(1)
{
wiced_rtos_pop_from_queue(&paddleQueue,&msg,WICED_WAIT_FOREVER);
switch(msg.evt)
{
case MSG_POSITION:
if(gameState == GS_RUNNING)
paddle0_desire_pos = msg.val/2;
if(gameState == GS_OVER)
{
paddle0_desire_pos = msg.val/2;
updatePaddle(PADDLE_ABSOLUTE);
}
break;
case MSG_BUTTON0:
if(gameState == GS_OVER || gameState == GS_START)
startGame();
break;
case MSG_BUTTON1:
break;
}
}
}

Program and Test

Now that it is all done… program and test it.

GameThread.c

Here is the whole thread is here so you can copy/paste it into your file.

#include "GameThread.h"
#include "cy_tft_display.h"
#include "SystemGlobal.h"
#include "ugui.h"
/******************************************************
*                      Macros
******************************************************/
#define UPDATE_SCREEN_TIME (20) // Update the screen every 20ms
#define SPEED (2)
#define SCREEN_X (320)
#define SCREEN_Y (240)
#define TOP_FIELD (21)
#define PD_WIDTH (10)
#define PD_LEN (70)
#define DOTS (3)
#define PADDLE0_COLOR (C_BLUE)
#define BALL_COLOR (C_GREEN)
#define BALL_SIZE (10)
/******************************************************
*                    Constants
******************************************************/
/******************************************************
*                   Enumerations
******************************************************/
/******************************************************
*                 Type Definitions
******************************************************/
// States of the game
typedef enum {
GS_SPLASH,
GS_START,
GS_RUNNING,
GS_OVER
} game_state_t;
// Methods to move the paddle
typedef enum {
PADDLE_INCREMENT,
PADDLE_ABSOLUTE
} paddle_update_t;
/******************************************************
*                    Structures
******************************************************/
/******************************************************
*               Static Function Declarations
******************************************************/
static void UG_PutStringCenter(uint32_t x, uint32_t y, uint32_t fontx, uint32_t fonty,char *string);
static void displaySplashScreen();
static void displayStartButton();
static void  displayStartScreen();
static void displayScore();
static void displaySpeed();
static void endGame();
static inline uint32_t calcPaddleTop();
static inline uint32_t calcPaddleBottom();
static void updatePaddle(paddle_update_t type);
static void updateBall();
static void updateScreen(void *arg);
/******************************************************
*               Variable Definitions
******************************************************/
static UG_GUI   gui;
static wiced_timer_t updateScreenTimer;
static uint32_t gameScore;
static game_state_t gameState;
// position of the paddle
static uint32_t paddle0_desire_pos=0;
static uint32_t paddle0_cur_pos=0;
// Position, direction and speed of the ball
static uint32_t ballx,bally;
static int32_t ballXdir, ballYdir;
static uint32_t ballSpeed;
/******************************************************
*               Functions
******************************************************/
// ARH Function to put text in the center of a point (UG_PutString does upper left)
static void UG_PutStringCenter(uint32_t x, uint32_t y, uint32_t fontx, uint32_t fonty,char *string)
{
y = y - fonty/2;
x = x - (strlen(string)/2)*fontx;
if(strlen(string)%2)
x = x - fontx/2;
UG_PutString(x,y,string);
}
// Display the splash screen
static void displaySplashScreen()
{
gameState = GS_SPLASH;
UG_FontSelect( &FONT_22X36 );
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/5,22,36,"Cypress");
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/5*2,22,36,"Mouser");
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/5*3,22,36,"PSoC 6");
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/5*4,22,36,"WICED 4343");
wiced_rtos_delay_milliseconds(2000);
}
// This function displays the start button message
static void displayStartButton()
{
UG_FontSelect(&FONT_12X20);
UG_PutStringCenter(SCREEN_X/2 , SCREEN_Y - 30 ,12,22,  "Press B0 To Start");
}
// Display the Start Screen
static void  displayStartScreen()
{
gameState = GS_START;
UG_FillScreen( C_BLACK );
UG_FontSelect( &FONT_22X36 );
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/2 -2 - 18 ,22,36,"Ready");
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/2 + 2 + 18 ,22,36,"Player 1");
displayStartButton();
}
// This function displays the score
static void displayScore()
{
char buff[10];
sprintf(buff,"%2X",(unsigned int)gameScore);
UG_FontSelect(&FONT_12X20);
UG_PutString( 75, 0, buff);
}
// This function displays the speed
static void displaySpeed()
{
char buff[10];
sprintf(buff,"%2X",(unsigned int)ballSpeed-1);
UG_FontSelect(&FONT_12X20);
UG_PutString( 275, 0, buff);
}
// This function initializes everything and starts a new game
static void startGame()
{
gameScore = 0;
paddle0_desire_pos = 50; // start the game with the paddle moving
paddle0_cur_pos = 0;
ballx = PD_WIDTH ;                   // start the ball on the paddle on the right of the screen
bally  = calcPaddleTop() + PD_LEN/2; // start the ball in the middle of the paddle
ballSpeed = SPEED;
ballXdir = ballSpeed;
ballYdir = ballSpeed;
UG_FillScreen( C_BLACK );  // clear screen
UG_FontSelect(&FONT_12X20);
UG_PutString( 0, 0,  "Score:");
displayScore();
UG_PutString(200,0,"Speed:");
displaySpeed();
UG_DrawLine(0,20,SCREEN_X,20,C_RED); // red line under text to represent top of play screen
gameState = GS_RUNNING;
wiced_rtos_start_timer(&updateScreenTimer); // Timer to update screen
}
// Stop the game
static void endGame()
{
gameState = GS_OVER;
wiced_rtos_stop_timer(&updateScreenTimer);
UG_FontSelect( &FONT_22X36 );
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/2,22,36,"Game Over");
displayStartButton();
}
// Figure out the y position of the top of the paddle
static inline uint32_t calcPaddleTop()
{
return (paddle0_cur_pos)*DOTS+TOP_FIELD;
}
// Figure out the y position of the bottom of the paddle
static inline uint32_t calcPaddleBottom()
{
return (paddle0_cur_pos)*DOTS+PD_LEN+TOP_FIELD;
}
// Move the paddle either to : PADDLE_INCREMENT the next location or PADDLE_ABSOLUTE - final location
static void updatePaddle(paddle_update_t type)
{
// If the paddle is where it is supposed to be then just return
if(paddle0_cur_pos == paddle0_desire_pos)
return;
// erase the current paddle
UG_FillFrame(0,calcPaddleTop(),PD_WIDTH,calcPaddleBottom(),C_BLACK);
switch (type)
{
case PADDLE_INCREMENT:
if(paddle0_cur_pos < paddle0_desire_pos)
paddle0_cur_pos += SPEED;
else
paddle0_cur_pos -= SPEED;
// If the paddle is within one move of the final spot, put it there
if(abs((int)paddle0_cur_pos-(int)paddle0_desire_pos) < SPEED)
paddle0_cur_pos = paddle0_desire_pos;
break;
case PADDLE_ABSOLUTE:
paddle0_cur_pos = paddle0_desire_pos;
break;
}
// draw the paddle
UG_FillFrame(0,calcPaddleTop(),PD_WIDTH,calcPaddleBottom(),PADDLE0_COLOR);
}
// Move the ball to the next location
static void updateBall()
{
static const uint32_t BallFudgeFactor=3;
UG_DrawCircle(ballx,bally,BALL_SIZE,C_BLACK);
ballx += ballXdir;
bally += ballYdir;
// Check to see if the ball hit the far right side
if(ballx > SCREEN_X - BALL_SIZE)
{
ballx = SCREEN_X - BALL_SIZE;
ballXdir = -ballSpeed;
}
// check to see if the ball hit the far left side... or the paddle
if(ballx < (BALL_SIZE + PD_WIDTH + BallFudgeFactor))
{
// See if the ball missed the paddle
if(bally + BALL_SIZE < calcPaddleTop() || bally - BALL_SIZE > calcPaddleBottom())
{
endGame();
//WPRINT_APP_INFO(("Missed Paddle\r\n"));
}
gameScore = gameScore + 1;
displayScore();
if(gameScore % 3 == 0) // Speed up every three hits
{
ballSpeed +=1;
displaySpeed();
}
ballx = BALL_SIZE + PD_WIDTH + BallFudgeFactor;
ballXdir = +ballSpeed;
}
// Check to see if the ball hit the top or bottom
if(bally > SCREEN_Y - BALL_SIZE) // bottom
{
bally = SCREEN_Y - BALL_SIZE;
ballYdir = -ballSpeed;
}
if(bally < TOP_FIELD+BALL_SIZE) // top
{
bally = BALL_SIZE+TOP_FIELD;
ballYdir = +ballSpeed;
}
UG_DrawCircle(ballx,bally,BALL_SIZE,BALL_COLOR);
}
// This function is called every UPADTE_SCREEN_TIME milliseconds by the updateScreenTimer
static void updateScreen(void *arg)
{
updatePaddle(PADDLE_INCREMENT);
updateBall();
}
// Main game thread
void gameThread(wiced_thread_arg_t arg)
{
game_msg_t msg;
Cy_TFT_Init();                                             // Init the TFT
UG_Init( &gui, Cy_TFT_displayDriver, SCREEN_X, SCREEN_Y ); // Connect the driver
UG_FillScreen( C_BLACK );   // Clear the screen
UG_SetBackcolor( C_BLACK );
UG_SetForecolor( C_WHITE );
wiced_rtos_init_timer(&updateScreenTimer,UPDATE_SCREEN_TIME,updateScreen,0);
displaySplashScreen();
displayStartScreen();
while(1)
{
wiced_rtos_pop_from_queue(&paddleQueue,&msg,WICED_WAIT_FOREVER);
switch(msg.evt)
{
case MSG_POSITION:
if(gameState == GS_RUNNING)
paddle0_desire_pos = msg.val/2;
if(gameState == GS_OVER)
{
paddle0_desire_pos = msg.val/2;
updatePaddle(PADDLE_ABSOLUTE);
}
break;
case MSG_BUTTON0:
if(gameState == GS_OVER || gameState == GS_START)
startGame();
break;
case MSG_BUTTON1:
break;
}
}
}

 

Mouser PSoC 6-WiFi-BT L3: Using the CY8CKIT-028-TFT Shield

Designing low-power, cloud-connected IoT devices with PSoC® 6 MCU’s and WICED® Wi-Fi/Bluetooth

Summary

In this lesson we will start making the game.  The first thing that it will need is a display and we will use the CY8CKIT-028-TFT.  In order to talk to the display we will use a library built into WICED called ugui.  That library needs a driver configuration which we will copy of out the code example we provide.  Finally we will start building a thread called the “GameThread” which will actually make up the game.

  1. Download CE222494_PSoC6_WICED_WiFi
  2. Copy the L2CapSense into L3CapSenseTft
  3. Copy cy_tft_display.c/h into the project
  4. Make a file GameThread.h
  5. Make a file GameThread.c
  6. Rename L2CapSense.mk to be L3CapSenseTft.mk & Fix
  7. Update main.c
  8. Test

Download CE222494_PSoC6_WICED_WiFi

If you click on the CY8CKIT-062-WiFi-BT webpage you will find that there are a bunch of files which are associated with the development kit, including CY8CKIT-062-WiFi-BT PSoC® 6 WiFi-BT Pioneer Kit Code Examples.zip.

Download that folder, then copy the directory into your WICED Studio Apps/WStudio folder.

Once you do that it should look like this:

Copy L3CapSense into L3CapSenseTft

Now copy/paste the L2CapSense project into a new project called L3CapSenseTft

Copy cy_tft_display.c/h into the project

Open up the CE222494 code example directory and copy the two files cy_tft_display.c andcy_tft_display.c which are drivers for the ugui library and then paste them into your new project L3CapSenseTft.

Make a file GameThread.h

Create a new file called GamThread.h and a definition of the GameThread which will be used by the main.c to get the game thread going.

#pragma once
#include "wiced.h"
void gameThread(wiced_thread_arg_t arg);

Make a file GameThread.c

Now create a file called GameThread.c it will have 5 functions.  Here is the whole file to make it simpler to copy and paste, but Ill explain each function one by one

#include "GameThread.h"
#include "cy_tft_display.h"
#define SCREEN_X (320)
#define SCREEN_Y (240)
static UG_GUI   gui;
// ARH Function to put text in the center of a point (UG_PutString does upper left)
static void UG_PutStringCenter(uint32_t x, uint32_t y, uint32_t fontx, uint32_t fonty,char *string)
{
y = y - fonty/2;
x = x - (strlen(string)/2)*fontx;
if(strlen(string)%2)
x = x - fontx/2;
UG_PutString(x,y,string);
}
// Display the splash screen
static void displaySplashScreen()
{
UG_FontSelect( &FONT_22X36 );
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/5,22,36,"Cypress");
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/5*2,22,36,"Mouser");
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/5*3,22,36,"PSoC 6");
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/5*4,22,36,"WICED 4343");
wiced_rtos_delay_milliseconds(2000);
}
// This function displays the start button message
static void displayStartButton()
{
UG_FontSelect(&FONT_12X20);
UG_PutStringCenter(SCREEN_X/2 , SCREEN_Y - 30 ,12,22,  "Press B0 To Start");
}
// Display the Start Screen
static void  displayStartScreen()
{
UG_FillScreen( C_BLACK );
UG_FontSelect( &FONT_22X36 );
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/2 -2 - 18 ,22,36,"Ready");
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/2 + 2 + 18 ,22,36,"Player 1");
displayStartButton();
}
// Main game thread
void gameThread(wiced_thread_arg_t arg)
{
Cy_TFT_Init();                                             // Init the TFT
UG_Init( &gui, Cy_TFT_displayDriver, SCREEN_X, SCREEN_Y ); // Connect the driver
UG_FillScreen( C_BLACK );   // Clear the screen
UG_SetBackcolor( C_BLACK );
UG_SetForecolor( C_WHITE );
displaySplashScreen();
displayStartScreen();
while(1)
{
wiced_rtos_delay_milliseconds(1000);
}
}

The main game thread function is: void gameThread(wiced_thread_arg_t arg).  This function

  1. Initializes the TFT
  2. Initializes the UGUI library
  3. Clears the screen (by setting it all black)
  4. Sets the colors to draw white on black
  5. Displays the splash screen (which takes 2 seconds)
  6. Displays the start screen
  7. Waits until the end of time
// Main game thread
void gameThread(wiced_thread_arg_t arg)
{
Cy_TFT_Init();                                             // Init the TFT
UG_Init( &gui, Cy_TFT_displayDriver, SCREEN_X, SCREEN_Y ); // Connect the driver
UG_FillScreen( C_BLACK );   // Clear the screen
UG_SetBackcolor( C_BLACK );
UG_SetForecolor( C_WHITE );
displaySplashScreen();
displayStartScreen();
while(1)
{
wiced_rtos_delay_milliseconds(1000);
}
}

The function displaySplashScreen simply sets the font, then draws 4 text strings, then waits for a few seconds… then moves on

// Display the splash screen
static void displaySplashScreen()
{
UG_FontSelect( &FONT_22X36 );
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/5,22,36,"Cypress");
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/5*2,22,36,"Mouser");
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/5*3,22,36,"PSoC 6");
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/5*4,22,36,"WICED 4343");
wiced_rtos_delay_milliseconds(2000);
}

The displayStartScreen put the “Ready Player 1 on the screen” and then tells the user to press the B0 to start the game.

// This function displays the start button message
static void displayStartButton()
{
UG_FontSelect(&FONT_12X20);
UG_PutStringCenter(SCREEN_X/2 , SCREEN_Y - 30 ,12,22,  "Press B0 To Start");
}
// Display the Start Screen
static void  displayStartScreen()
{
UG_FillScreen( C_BLACK );
UG_FontSelect( &FONT_22X36 );
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/2 -2 - 18 ,22,36,"Ready");
UG_PutStringCenter(SCREEN_X/2,SCREEN_Y/2 + 2 + 18 ,22,36,"Player 1");
displayStartButton();
}

The U8G_PutString function uses coordinates x and y to set the upper left of the text.  For formatting purposes it is easier for me to think about the middle of the string.  This function just calculates the upper left (x,y) given the middle center (x,y).  To do this you need to also know the (x,y) size of the font.

static void UG_PutStringCenter(uint32_t x, uint32_t y, uint32_t fontx, uint32_t fonty,char *string)

// ARH Function to put text in the center of a point (UG_PutString does upper left)
static void UG_PutStringCenter(uint32_t x, uint32_t y, uint32_t fontx, uint32_t fonty,char *string)
{
y = y - fonty/2;
x = x - (strlen(string)/2)*fontx;
if(strlen(string)%2)
x = x - fontx/2;
UG_PutString(x,y,string);
}

Rename L2CapSense.mk to be L3CapSenseTft.mk & Fix

To make this build we need to modify the makefile to know about the new thread as well as the tft driver.  In addition we need to tell the linker to link with the graphics library.

NAME := App_WStudio_L3CapSenseTft
$(NAME)_SOURCES := 	main.c \
CapSenseThread.c \
GameThread.c \
cy_tft_display.c
$(NAME)_COMPONENTS := graphics/ugui

Update main.c

In main.c I will:

  1. Include the GameThread.h
  2. Add a variable to hold the gameThreadHandle
  3. Then start the gameThread
#include "wiced.h"
#include "CapSenseThread.h"
#include "GameThread.h"
/******************************************************
*                      Macros
******************************************************/
/******************************************************
*                    Constants
******************************************************/
/******************************************************
*                   Enumerations
******************************************************/
/******************************************************
*                 Type Definitions
******************************************************/
/******************************************************
*                    Structures
******************************************************/
/******************************************************
*               Static Function Declarations
******************************************************/
/******************************************************
*               Variable Definitions
******************************************************/
wiced_thread_t blinkThreadHandle;
wiced_thread_t capSenseThreadHandle;
wiced_thread_t gameThreadHandle;
/******************************************************
*               Function Definitions
******************************************************/
void pdlBlinkThread(wiced_thread_arg_t arg)
{
while(1)
{
Cy_GPIO_Inv(GPIO_PRT0,3);
wiced_rtos_delay_milliseconds(500);
}
}
void application_start( )
{
wiced_init();
WPRINT_APP_INFO(("Started Application\n"));
wiced_rtos_create_thread(&blinkThreadHandle,7,"Blink Thread",pdlBlinkThread,500,0);
wiced_rtos_create_thread(&capSenseThreadHandle,7,"CapSense Thread",capSenseThread,1024,0);
wiced_rtos_create_thread(&gameThreadHandle,7,"game Thread",gameThread,4096,0);
}

Test

Now it is ready to test.  So create a Make Target, then Build and Program.  Hopefully you are now Ready Player 1.

 

Mouser PSoC 6-WiFi-BT L2 : WICED Studio & CapSense

Designing low-power, cloud-connected IoT devices with PSoC® 6 MCU’s and WICED® Wi-Fi/Bluetooth

Summary

In this lesson we will build your first WICED Studio project (the blinking LED)  and make sure that you can program the development kit.  Then we will update the project to include a thread for managing the CapSense block.  This thread will be carried into the other projects.

To implement this lesson I will follow these steps:

  1. Start WICED Studio 6.2
  2. Select 43xxx
  3. Create a folder called L2CapSense
  4. Create main.c and build a blinking LED thread
  5. Create L2CapSense.mk
  6. Create a make target
  7. Build Program and test it
  8. Create CapSenseThread.c
  9. Create CapSenseThread.h
  10. Update main.c
  11. Update the makefile
  12. Build Program and Test

Create the L2CapSense Folder

Create main.c

Right click on the folder and create a new file.  Name it L2CapSense

Insert the blinking LED code into main.c

#include "wiced.h"
/******************************************************
*                      Macros
******************************************************/
/******************************************************
*                    Constants
******************************************************/
/******************************************************
*                   Enumerations
******************************************************/
/******************************************************
*                 Type Definitions
******************************************************/
/******************************************************
*                    Structures
******************************************************/
/******************************************************
*               Static Function Declarations
******************************************************/
/******************************************************
*               Variable Definitions
******************************************************/
wiced_thread_t blinkThreadHandle;
/******************************************************
*               Function Definitions
******************************************************/
void pdlBlinkThread(wiced_thread_arg_t arg)
{
while(1)
{
Cy_GPIO_Inv(GPIO_PRT0,3);
wiced_rtos_delay_milliseconds(500);
}
}
void application_start( )
{
WPRINT_APP_INFO(("Started Application\n"));
wiced_rtos_create_thread(&blinkThreadHandle,7,"Blink Thread",pdlBlinkThread,500,0);
}

Create L2CapSense.mk

Create a makefile called L2CapSense.mk

Put the build information into the L2CapSense.mk

NAME := App_WStudio_L2CapSense
$(NAME)_SOURCES := 	main.c

Create a Make Target to run the project

Build and Test the Blinking LED

Create/Edit a File called CapSenseThread.c

#include "wiced.h"
void capSenseThread(wiced_thread_arg_t arg)
{
CapSense_Start();
CapSense_ScanAllWidgets();
while(1)
{
if(!CapSense_IsBusy())
{
CapSense_ProcessAllWidgets();
if(CapSense_IsWidgetActive(CapSense_BUTTON0_WDGT_ID))
{
WPRINT_APP_INFO(("Button 0 Active\n"));
}
if(CapSense_IsWidgetActive(CapSense_BUTTON1_WDGT_ID))
{
WPRINT_APP_INFO(("Button 1 Active\n"));
}
uint32_t val = CapSense_GetCentroidPos(CapSense_LINEARSLIDER0_WDGT_ID);
if(val < 0xFFFF)
{
WPRINT_APP_INFO(("Slider = %d\n",(int)val));
}
CapSense_ScanAllWidgets();
}
wiced_rtos_delay_milliseconds(25); // Poll every 25ms (actual scan time ~8ms)
}
}

Create/Edit a File Called CapSenseThread.h

#pragma once
#include "wiced.h"
void capSenseThread(wiced_thread_arg_t arg);

Update main.c

#include "wiced.h"
#include "CapSenseThread.h"

Add a variable to hold the handle for the capSenseThread at the top of main.c

wiced_thread_t capSenseThreadHandle;

Update the main function to start the CapSenseThread

void application_start( )
{
WPRINT_APP_INFO(("Started Application\n"));
wiced_rtos_create_thread(&blinkThreadHandle,7,"Blink Thread",pdlBlinkThread,500,0);
wiced_rtos_create_thread(&capSenseThreadHandle,7,"CapSense Thread",capSenseThread,1024,0);
}

Update the L2CapsenseThread.mk

NAME := App_WStudio_L2CapSense
$(NAME)_SOURCES := 	main.c \
CapSenseThread.c

Build, Program and Test the CapSenseThread