Tilt Hydrometer (Part 9) An LCD Display

Summary

This article updates the Tilt Hydrometer project to include an LCD display using the Segger emWin library.  This includes a discussion of a “new” library in the suite of IoT Expert awesome sauce.  Specifically the IoT Expert Graphics library.

This series is broken up into the following 12 articles with a few additional possible articles. 

Tilt Hydrometer (Part 1) Overview & Out-of-Box

Tilt Hydrometer (Part 2) Architecture

Tilt Hydrometer (Part 3) Advertising Scanner

Tilt Hydrometer (Part 4) Advertising Packet Error?

Tilt Hydrometer (Part 5) Tilt Simulator & Multi Advertising iBeacons

Tilt Hydrometer (Part 6) Tilt Simulator an Upgrade

Tilt Hydrometer (Part 7) Advertising Database

Tilt Hydrometer (Part 8) Read the Database

Tilt Hydrometer (Part 9) An LCD Display

Tilt Hydrometer (Part 10) The Display State Machine

Tilt Hydrometer (Part 11) Draw the Display Screens

Tilt Hydrometer (Part 12) CapSense

Tilt Hydrometer: LittleFS & SPI Flash (Part ?)

Tilt Hydrometer: WiFi Introducer (Part ?)

Tilt Hydrometer: WebServer (Part ?)

Tilt Hydrometer: Amazon MQTT (Part ?)

Tilt Hydrometer: Printed Circuit Board (Part ?)

You can get the source code from git@github.com:iotexpert/Tilt2.git  This repository has tags for each of the articles which can be accessed with "git checkout part12"  You can find the Tilt Simulator at  git@github.com:iotexpert/TiltSimulator.git.

 

Here is the architecture with the blocks we are editing marked green.

Story

I knew that I wanted to use the TFT Display so that I could have this Tilt Hydrometer system sitting in my “brewery”.  Notice in the picture below you can see that the screen is currently displaying the data from the “black” tilt.  Also notice that you can see two fermenters in the background in the brewery.  Who needs a guest bathroom in the era of Covid 🙂

Also notice that there is a Raspberry Pi (more on that in a future article).  And for all of you married people out there, yes I have a very frustrated but forgiving wife.

Add Display Libraries & Fix the Makefile

The first step in building this is to add the new libraries.  Specifically the display-tft-st7789v library which knows how to talk to the lcd and the emWin library.  AND the new IoT Expert Graphics Library.

In order to use the emWin library you need to fix the Makefile to include the emWin component.  Notice that I chose OS  meaning an RTOS and NTS meaning no touch screen.

COMPONENTS=FREERTOS WICED_BLE EMWIN_OSNTS

IoT Expert Graphics Library

I wanted to have a splash screen on this project with my Logo.  So I started digging around on my computer to find the graphics, which I found in the file IOTexpert_Logo_Vertical.png.  Unfortunately this is a 1091×739 portable network graphics file, not exactly what emWin wants to draw with.  To fix this I opened it in GIMP

Then I resized it to my screen size (320×240)

Now it looks like this:

Then I saved it as a PNG.  Still doesn’t work with emWin.  But, if you look in the emWin library you will notice that there is a new directory called “Tool”

This directory contains the Segger tools for supporting their graphics library.  The one that I want is called “Bin2C.exe” (unfortunately it is a Windows only tool).  I run it and open the logo.

Then I click save and pick “C” bitmap file (*.c)

For my display I am using 16-bit color, also known as High Color with Alpha 565.

The tool nicely makes me a file with the right stuff in it.  First, the array of unsigned char (representing the bitmap) and the GUI_BITMAP structure.

Unfortunately it didn’t create a header file which I do like this (notice I renamed the data)

#pragma once
#include <stdlib.h>

#include "GUI.h"

extern GUI_CONST_STORAGE GUI_BITMAP bmIOTexpert_Logo_Vertical320x240;

And, if you have been paying attention you might have noticed that I created a new IoT Expert Library – Graphics that contains these new files. (I have explained this process several times in the past)

TFT Pins Discussion

I am planning on making a custom circuit board for this whole project.  So, I didn’t want to use the whole CY8CKIT-028-TFT library.  In order to start the TFT you need to call the function “mtb_st7789v()” with an argument of type “mtb_st7789v_pins_t”.  When Cypress created the BSP for the TFT we defined a function like this in the header file.

/**
 * Initializes GPIOs for the software i8080 interface.
 * @return CY_RSLT_SUCCESS if successfully initialized, else an error about
 * what went wrong
 */
static inline cy_rslt_t cy_tft_io_init(void)
{
    static const mtb_st7789v_pins_t PINS =
    {
        .db08 = CY8CKIT_028_TFT_PIN_DISPLAY_DB8,
        .db09 = CY8CKIT_028_TFT_PIN_DISPLAY_DB9,
        .db10 = CY8CKIT_028_TFT_PIN_DISPLAY_DB10,
        .db11 = CY8CKIT_028_TFT_PIN_DISPLAY_DB11,
        .db12 = CY8CKIT_028_TFT_PIN_DISPLAY_DB12,
        .db13 = CY8CKIT_028_TFT_PIN_DISPLAY_DB13,
        .db14 = CY8CKIT_028_TFT_PIN_DISPLAY_DB14,
        .db15 = CY8CKIT_028_TFT_PIN_DISPLAY_DB15,
        .nrd = CY8CKIT_028_TFT_PIN_DISPLAY_NRD,
        .nwr = CY8CKIT_028_TFT_PIN_DISPLAY_NWR,
        .dc = CY8CKIT_028_TFT_PIN_DISPLAY_DC,
        .rst = CY8CKIT_028_TFT_PIN_DISPLAY_RST,
    };
    return mtb_st7789v_init8(&PINS);
}

Curiously we also made a static definition inside of the .c file.

static const mtb_st7789v_pins_t tft_pins =
{
    .db08 = CY8CKIT_028_TFT_PIN_DISPLAY_DB8,
    .db09 = CY8CKIT_028_TFT_PIN_DISPLAY_DB9,
    .db10 = CY8CKIT_028_TFT_PIN_DISPLAY_DB10,
    .db11 = CY8CKIT_028_TFT_PIN_DISPLAY_DB11,
    .db12 = CY8CKIT_028_TFT_PIN_DISPLAY_DB12,
    .db13 = CY8CKIT_028_TFT_PIN_DISPLAY_DB13,
    .db14 = CY8CKIT_028_TFT_PIN_DISPLAY_DB14,
    .db15 = CY8CKIT_028_TFT_PIN_DISPLAY_DB15,
    .nrd = CY8CKIT_028_TFT_PIN_DISPLAY_NRD,
    .nwr = CY8CKIT_028_TFT_PIN_DISPLAY_NWR,
    .dc = CY8CKIT_028_TFT_PIN_DISPLAY_DC,
    .rst = CY8CKIT_028_TFT_PIN_DISPLAY_RST,
};

What I know is that I need those pin definitions, so I will copy them into my project (a bit later in this article)

Make a New Display Task

To get this going, make a file called displayManager.h to contain the public definition of the task.

#pragma once

void dm_task(void *arg);

Then add a file displayManager.c.  This file will be the focus of the rest of this article.  Before we get too far down the road building all of the functionality of the display, first lets setup a test task to verify the functionality of the libraries and the screen.  As I talked about earlier, define the pins for the driver.  These the pin names come out of the BSP and “should” work for all of the Cypress development kits (that have Arduino headers).

#include <stdio.h>
#include <stdlib.h>

#include "GUI.h"
#include "mtb_st7789v.h"
#include "cybsp.h"

#include "FreeRTOS.h"
#include "task.h"

#include "displayManager.h"

#include "IOTexpert_Logo_Vertical320x240.h"

const mtb_st7789v_pins_t tft_pins =
{
    .db08 = (CYBSP_J2_2),
    .db09 = (CYBSP_J2_4),
    .db10 = (CYBSP_J2_6),
    .db11 = (CYBSP_J2_10),
    .db12 = (CYBSP_J2_12),
    .db13 = (CYBSP_D7),
    .db14 = (CYBSP_D8),
    .db15 = (CYBSP_D9),
    .nrd  = (CYBSP_D10),
    .nwr  = (CYBSP_D11),
    .dc   = (CYBSP_D12),
    .rst  = (CYBSP_D13)
};

Then make a simple task that

  1. Initializes the driver
  2. Initialize emWin
  3. Sets the background color
  4. Clear the screen
  5. Draw the IoT Expert Logo
void dm_task(void *arg)
{
    /* Initialize the display controller */
    mtb_st7789v_init8(&tft_pins);
    GUI_Init();
    GUI_SetBkColor(GUI_WHITE);
    GUI_Clear();
    GUI_DrawBitmap(&bmIOTexpert_Logo_Vertical320x240,0,11);

    while(1)
        vTaskDelay(portMAX_DELAY);
}

Test

When I program this project I get a nice white background screen with my beautiful logo on it. Sweet.  In the next article I’ll do some work on displaying useful information on the screen.

 

Tilt Hydrometer (Part 8) Read the Database

Summary

This article expands the functionality of the Tilt Database by adding functions to get the status of Tilts from other threads in the system.  It demonstrates the FreeRTOS queue’s to provide a generic mechanism for communication between threads.

This series is broken up into the following 12 articles with a few additional possible articles. 

Tilt Hydrometer (Part 1) Overview & Out-of-Box

Tilt Hydrometer (Part 2) Architecture

Tilt Hydrometer (Part 3) Advertising Scanner

Tilt Hydrometer (Part 4) Advertising Packet Error?

Tilt Hydrometer (Part 5) Tilt Simulator & Multi Advertising iBeacons

Tilt Hydrometer (Part 6) Tilt Simulator an Upgrade

Tilt Hydrometer (Part 7) Advertising Database

Tilt Hydrometer (Part 8) Read the Database

Tilt Hydrometer (Part 9) An LCD Display

Tilt Hydrometer (Part 10) The Display State Machine

Tilt Hydrometer (Part 11) Draw the Display Screens

Tilt Hydrometer (Part 12) CapSense

Tilt Hydrometer: LittleFS & SPI Flash (Part ?)

Tilt Hydrometer: WiFi Introducer (Part ?)

Tilt Hydrometer: WebServer (Part ?)

Tilt Hydrometer: Amazon MQTT (Part ?)

Tilt Hydrometer: Printed Circuit Board (Part ?)

You can get the source code from git@github.com:iotexpert/Tilt2.git  This repository has tags for each of the articles which can be accessed with "git checkout part12"  You can find the Tilt Simulator at  git@github.com:iotexpert/TiltSimulator.git.

 

Story

At this point the Tilt Data Manager has all of the knowledge of the status of Tilts.  But, we have other threads which need access to that data.  How are they going to get it in a thread safe way?  Moreover, how am I going to provide access to this data without hardcoding specific information about the threads into the Tilt Data Manager?  In this article I add functionality to the Tilt Data Manager Thread to provide Tilt status.  Then I will test the functionality by adding test code into the ntshell.  The green boxes are the functional blocks I will touch in this update.

 

Update the Public Interface to tiltDataManager.h

As I thought about what the other threads in the system would need I decided the best thing to do would be to write their public APIs.  Here it is:

  • What Color is the “Tilt” i.e. “Pink”
  • How many Tilts are in the system (so that you can loop through them)
  • What Tilts are currently active – a bitmask
  • How many events have been seen for each Tilt
  • Please give me a copy of the current data.
/////////////// Generally callable threadsafe - non blocking
char *tdm_colorString(tdm_tiltHandle_t handle);       // Return a char * to the color string for the tilt handle
int tdm_getNumTilt();                                 // Returns the number of possible tilts (probably always 8)
uint32_t tdm_getActiveTiltMask();                     // Return a bitmask of the active handles
uint32_t tdm_getNumDataSeen(tdm_tiltHandle_t handle); // Return number of data points seen
tdm_tiltData_t *tdm_getTiltData(tdm_tiltHandle_t handle);

Create the Thread Unsafe Functions

Obviously you need to do things thread safely.  However, I decided to have a few functions which couldn’t hurt anything, but are unsafe.  (Sorry Butch… comment below if you don’t agree and Ill fix it).  To make this a bit safer, I set the data as volatile to make the compiler at least write it back into memory when things are done.

typedef struct  {
    char *colorName;
    uint8_t uuid[TILT_IBEACON_HEADER_LEN];
    volatile tdm_tiltData_t *data;
    volatile int numDataPoints;
    volatile int numDataSeen;
} tilt_t;

The functions are very straight forward.

char *tdm_colorString(tdm_tiltHandle_t handle)
{
    return tiltDB[handle].colorName;
}

int tdm_getNumTilt()
{
    return NUM_TILT;
}

uint32_t tdm_getActiveTiltMask()
{
    uint32_t mask=0;
    for(int i=0;i<NUM_TILT;i++)
    {
        if(tiltDB[i].data)
            mask |= 1<<i;
    }
    return mask;
}

uint32_t tdm_getNumDataSeen(tdm_tiltHandle_t handle)
{
    return tiltDB[handle].numDataSeen;
}

Thread Safe Communication

Now, a much better answer for thread safety.  If you have multiple tasks that need access to the same data it is very tempting to just communicate between the threads with global variables (see above).  You are just asking for trouble because one task might be partially through updating data, when it is interrupted by the RTOS scheduler.  Then another threads starts reading/writing from global variables that were incompletely updated by the suspended thread.  BAD!

So, what is a person to do?

What I typically do is use an RTOS queue to communicate between the two tasks.  In the picture below you can see that the ntshell will send a message to the Tilt Data Manager that it wants to know the data from a Tilt (using the handle) and it wants the response sent back to the “response queue”.  When the Tilt Data Manager gets this command, it will build a response and push it back into the “response queue”.  With this scheme the Tilt Data Manager does not know which thread that it is talking to, just the handle of the queue.

Create the Thread Safe Function

Inside of my code, the first step in creating the thread safe communication is to add a command to the Tilt Commands, TDM_CMD_GET_DATAPOINT

typedef enum {
    TDM_CMD_ADD_DATAPOINT,
    TDM_CMD_PROCESS_DATA,
    TDM_CMD_GET_DATAPOINT,
} tdm_cmd_t;

The response to all questions about Tilts will be in the form of a “tdm_tiltData_t” structure.  Specifically one that has been malloc’d on the heap.  To support this I will create a function that:

  1. Mallocs a new structure
  2. Copys the data
  3. Fix the averaging
  4. Returns a pointer to the new data
// This function returns a malloc'd copy of the front of the most recent datapoint ... this function should only be used 
// internally because it is not thread safe.
static tdm_tiltData_t *tdm_getDataPointCopy(tdm_tiltHandle_t handle)
{
    tdm_tiltData_t *dp;
    dp = malloc(sizeof(tdm_tiltData_t));
    memcpy(dp,(tdm_tiltData_t *)tiltDB[handle].data,sizeof(tdm_tiltData_t));

    dp->gravity = dp->gravity / tiltDB[handle].numDataPoints;
    dp->temperature = dp->temperature / tiltDB[handle].numDataPoints;

    return dp;
}

Then I update the command queue to deal with the get data message.

            case TDM_CMD_GET_DATAPOINT:
                dp = tdm_getDataPointCopy(msg.tilt);
                xQueueSend(msg.msg,&dp,10);
            break;

Then I create the public function for other threads to call the send the request for the data command.  This function is called in the context of the calling thread NOT the tiltDataManager.  It will create a temporary queue for the transaction, send the command, then sit and wait for the response.

tdm_tiltData_t *tdm_getTiltData(tdm_tiltHandle_t handle)
{
    QueueHandle_t respqueue;
    tdm_tiltData_t *rsp;

    if(handle<0 || handle>=NUM_TILT || tiltDB[handle].data == 0 )
        return 0;

    respqueue = xQueueCreate(1,sizeof(tdm_tiltData_t *));
    if(respqueue == 0)
        return 0;

    tdm_cmdMsg_t msg;
    msg.msg = respqueue;
    msg.tilt = handle;
    msg.cmd =  TDM_CMD_GET_DATAPOINT;
    if(xQueueSend(tdm_cmdQueue,&msg,0) != pdTRUE)
    {
        printf("failed to send to dmQueue\n");
    }
    xQueueReceive(respqueue,(void *)&rsp,portMAX_DELAY);
    vQueueDelete(respqueue);
    return rsp;
}

Test with the ntshell

To test things whole thing out I add a new command to ntshell.  This command just try all of the public APIs

static int usrcmd_testdm(int argc, char **argv)
{
    tdm_tiltData_t *msg;

    printf("NumTilt = %d\n",tdm_getNumTilt());
    printf("Active = %X\n",(unsigned int)tdm_getActiveTiltMask());
    
    for(int i =0;i<tdm_getNumTilt();i++)
    {
        printf("Color = %s\t#Seen=%d\t",
            tdm_getColorString(i),
            (int)tdm_getNumDataSeen(i));

        if(tdm_getActiveTiltMask() & 1<<i)
        {
            msg = tdm_getTiltData(i);
            if(msg)
            {
                printf("G=%f\tT=%d",
                    msg->gravity,
                    (int)msg->temperature);
                free(msg);
            }
        }
        printf("\n");
    }
    return 0;
}

Here it is running.  Notice that I typed the command “testdm”  And that my test setup has heard two Tilts (remember I have a test broadcaster)

In the next article Ill add the TFT Display.