Stupid Python Tricks: Install MBED-CLI on macOS

Summary

This article describes a good set of steps to install the ARM MBED-CLI onto your Mac into a Python Virtual Environment.  You COULD make MBED-CLI work on your Mac a bunch of other/different ways, but, what I describe in this article is the best way.  You could follow the specific ARM instructions here.

The big steps are:

  1. Install Homebrew
  2. Install Python3
  3. Create a Virtual Python Environment
  4. Use PIP to Install MBED-CLI
  5. Download the GCC Compiler
  6. Configure the Compiler
  7. Test

macOS Homebrew & Python3

You need to be able to run Python, and in my opinion, specifically Python 3.  There are problems with doing this, first most of the macOS versions do not have Python 3 built in (and the Python 2 is old).  But, on macOS Catalina, you could use the macOS built-in Python3 and install MBED-CLI into the system libraries.  But, I believe that you should never make changes to the macOS system which could collide with its normal functioning.  I think that the easiest way to get a standalone Python onto your computer is to use “Homebrew” package manager.   Homebrew will enable you to easily install Python3 (and a bunch of other stuff) into /usr/local/bin.

To install Homebrew run:

  • /usr/bin/ruby -e “$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)”

And after a bunch of streaming console messages, it finishes:

Now you can use Homebrew to install python3 into /usr/local/bin by running:

  • brew install python3

And it puts out a bunch of stuff on the console… finally ending like this:

Now you should be able to “which python3” and see that python 3.7.6 is installed.

Create a Virtual Environment and Install mbed-cli

I always use a virtual environment when I run python to insure that all of the packages I install are together.  Run:

Command Comment
mkdir mbed-cli-example Create a directory to hold the virtual environments
cd mbed-cli-example
python3 -m venv venv Create the virtual environment in the directory called "venv"
source venv/bin/activate Activate the virtual environment
pip install mbed-cli Download the Mbed-cli package and install it into the virtual environment

Here is what is looks like:

Once that is done you can run “which mbed” where you will find that the MBED-CLI is in your virtual environment path.  And when you run “mbed” you should see all of the options.

Download the GCC Compiler

In order to actually do something with the MBED-CLI you also need to have an ARM compiler toolchain installed e.g. GCC.  The easiest (best?) way to get this done is download it from this the ARM website here.

After you download the Mac OS X 64-bit Tarball file you will end up with a “…tar.bz2” file in your Downloads directory.  When you double click the file, the finder will unzip and untar the file into a folder with all of the stuff you need for the Mac.

I have a directory on my Mac called ~/proj where I hold all of my “project” stuff.  So after downloading and untaring the file, I move it to my project directory with “mv ~/download/gcc-arm-none-eabi-9-2019-q4-major ~/proj”

Configure the Compiler

The final step is to tell the MBED-CLI where your toolchain is located.  There are a bunch of slightly different variants for how to do this.  BUT I think that the best is to change the mbed global configuration by running:

  • mbed config –global GCC_ARM_PATH ~/proj/gcc-arm-none-eabi-9-2019-q4-major/bin

The “–global” flag will store that configuration variable in the file ~/.mbed/.mbed (notice a directory called .mbed and a file called .mbed)

It took me a while to figure out which directory you should put in the path because there are multiple bin directories in the “gcc-arm…” directory.

If you want to use one of the other toolchains mbed has configuration variables for those as well:

You can also configure these paths with environment variables, but I dont like that.

macOS Gatekeeper

On macOS there is a security infrastructure called Gatekeeper that will prevent you from running applications that Gatekeeper doesn’t know about.  This includes the GCC toolchain (which you just downloaded).  To make it so that you can run the toolchain there are a couple of things you COULD do, but I think that the best thing to do is tell Gatekeeper that the Terminal program is exempt from the Gatekeeper rules.

To do this type the following command into a terminal: “spctl developer-mode enable-terminal”

Then go to the System Preferences –> Security & Privacy –> Developer Tools and check that the “Terminal” is exempt from the Gatekeeper Rules.

You can read more here and here.

Test

To test everything you have done, you should:

Command Comment
mbed import mbed-os-example-blinky Load the example project.  Notice that mbed also will install the Python requirements into your virtual environment.  It does this by running "pip install -r mbed-os/requirements.txt"
cd mbed-os-example-blinky
mbed config target CY8CKIT_062S2_43012 Setup the target (in my case the really cool P6 43012 devout)
mbed config toolchain GCC_ARM Set the toolchain to the one we setup in the previous step
mbed detect See if mbed can detect the development kit… it can

Here is what it looks like:

You can now run the compiler (with the -f option to program).  It should start up and barf out the compile messages.

Eventually it ends… then programs the development kit

The last thing to do is turn off the virtual environment

Which can later be turned back on with “source venv/bin/activate”

NTShell for MBED OS

Summary

Sometimes when you are building an IoT system it is very convenient to have a command line interface.  This enables you to use the keyboard to run functions inside of your program.  I often implement a really simple command processor that looks like this:

while(1)
{
   c=getchar();
   switch(c)
   {
      case 'a':
         doSomeCommand();
      break;

      case 'b':
         doSomeOtherCommand();
      break;
   }
}

I generally do this because it is really simple and doesn’t require much work and is easily typed and doesn’t require a parser.  However, this implementation is not very rich as it doesn’t deal with more interesting commands, like ones with arguments.  This weekend while I was looking around I found the Natural Tiny Shell an open source project by Shinichiro Nakamura.  I thought that it was pretty cool, so I ported it to PSoC and MBED OS.  And, after using it a bit I decided to turn it into an MBED OS library so that it was easy to import into new projects.

This article has the following parts

  1. A Survey of the NT Shell Code
  2. Creating a MBED OS Library
  3. Using the Template Project to make an MBED OS Program
  4. Adding new commands to the shell

A Survey of the NT Shell Code

On the NTShell website there is a picture of the architecture of the code.   This picture is trying to show several things

  1. Your application code talks to:
    1. ntshell = the actual guts of the shell
    2. ntopt = a command line argument parser used by your program to figure out what commands arguments have been given to the shell
    3. ntstdio = a set of wrapper functions around stdio that allows for “multiple” standard I/Os
  2. vtsend and vtrecv a library of functions to control vt100 terminals
  3. ntlibc – a set of ntshell specific implementation of libc functions like strlen, strcmp, etc
  4. ntconf – #defines that setup the parameters for the system

He also provides a call graph of the functions.  You can see from this graph that you are responsible only for providing

  1. func_read – read 1 character from the input device, in my case the uart
  2. func_write – write 1 character to the output device
  3. func_callback – a function that will be called by the shell when a command is matched by the shell and you need to do something with it.

You call

  1. ntshell_init – to setup the shell
  2. ntshell_set_promopt – to setup the prompt (imagine that)
  3. ntshell_execute – to run the shell (it never returns from this function)

 

Creating a MBED OS Library

I wanted to create a github library that I could mbed add.  So, I started by downloading the source code from the CuBeatSystems ntshell download site. I ended up with a tar file which needed to be expanded. Once done you will have 3 basic directories:

  1. sample = two example implementations
  2. lib = the actual c-source code for the shell
  3. util = contains c-source files for managing stdio and parsing argument strings

To make this more usable for MBED OS I decided to do several things

  1. Create a github library that can put into your project with  “mbed add”
  2. Create the porting layer functions func_read and func_write and put them in an “mbed” directory in the library
  3. Create a template for the user command functions (usrcmd.h/c)
  4. Create a template with the ntshell in a thread
  5. Create a template main.cpp

Porting Layer

As I described earlier you need the porting function func_read, func_write and the nutshell callback  to make the shell work.  These function reside in mbed-os-ntshell/mbed/util/mbed_port.h/c  The read function just uses the mbed stdio command “getchar” to read the correct number of characters.  And the ntshell_write uses the “putchar” to go the other way.

#include "ntshell.h"
#include "ntlibc.h"
#include "usrcmd.h"
#include <stdio.h>

int ntshell_read(char *buf, int cnt, void *extobj)
{
  int i;
  (void)extobj;
  for (i = 0; i < cnt; i++) {
    buf[i] = getchar();
  }
  return cnt;
}

int ntshell_write(const char *buf, int cnt, void *extobj)
{
  int i;
  (void)extobj;
  for (i = 0; i < cnt; i++) {
    putchar(buf[i]);
  }
  return cnt;
}

The callback function simply called into the user command module (which I provided a template for) with the command that was run by the user.

int ntshell_callback(const char *text, void *extobj)
{
  ntshell_t *ntshell = (ntshell_t *)extobj;
  (void)ntshell;
  (void)extobj;
  if (ntlibc_strlen(text) > 0) {

      usrcmd_execute(text);
  }
  return 0;
}

After I took these step I created a github site to hold it all.  You can use:

or

Using the NT Shell Library and Template in your Project:

The steps to use the NT Shell Library are:

  1. Create a new MBED OS Project
  2. Add the library to your project
  3. Copy the template files into your project
  4. Modify your main.cpp to start the ntShellThread
  5. Add new commands to the shell

1. Create a new MBED OS project

  • mbed new testNTShell

2. Add the NT Shell Library to your project:

  • mbed add git@github.com:iotexpert/mbed-os-ntshell.git

or

  • mbed add https://github.com/iotexpert/mbed-os-ntshell.git

3. Copy the template files into your project

Inside of the library there is a directory called template.  This directory has the following files:

I want to use all of these files to kickstart my project.  So I run:

  • cp mbed-os-ntshell/template/* .

4. Use main.cpp to start the ntShellThread

One of the files in the template directory was a main.cpp.  This program does two things

  1. Starts up the NT Shell Thread
  2. Starts 1Hz blinking LED in the main thread
#include "mbed.h"
#include "ntShellThread.h"

Thread ntShellThreadHandle;

DigitalOut led1(LED1);

int main()
{
    printf("Started NTShellThread\n");
    ntShellThreadHandle.start(ntShellThread);
    while(1)
    {
        led1 = !led1;
        wait(0.5);
    }

}

Add new commands to the shell

I also provide you template files for the user commands in the directory mbed-os-ntshell/template/usrcmd.h&c.  To add your own commands you need to:

  1. A function prototype for your command
  2. Add your command to the command table
  3. Create the actual function for your command.

The function prototype must match and will look like this (notice there are four commands in my template)

typedef int (*USRCMDFUNC)(int argc, char **argv);

static int usrcmd_help(int argc, char **argv);
static int usrcmd_info(int argc, char **argv);
static int usrcmd_clear(int argc, char **argv);
static int usrcmd_printargs(int argc, char **argv);

And the command table looks like this:

typedef struct {
    char *cmd;
    char *desc;
    USRCMDFUNC func;
} cmd_table_t;

static const cmd_table_t cmdlist[] = {
    { "help", "This is a description text string for help command.", usrcmd_help },
    { "info", "This is a description text string for info command.", usrcmd_info },
    { "clear", "Clear the screen", usrcmd_clear },
    { "printargs","print the list of arguments", usrcmd_printargs},

};

Finally your command function will look like this command which just prints out the commands.

static int usrcmd_printargs(int argc, char **argv)
{
    printf("ARGC = %d\n",argc);

    for(int i =0;i<argc;i++)
    {
        printf("argv[%d] = %s\n",i,argv[i]);
    }
    return 0;

}

MBED OS & CY8CKIT_062S2_43012 & Segger emWin & NTP

Summary

Have you ever wondered about the nature of time?  Given the demographics of my readers I am quite sure that all of you have pondered this topic.  In this article I will solve one of the great mysteries of human kind.  What time is it?  Well that may be a little bit over dramatic 🙂  Actually what I will show you is how to use the CY8CKIT-062S2-43012 development kit to get time from the internet using the Network Time Protocol (NTP), update the PSoC 6 RTC and display it on the CY8CKIT-028-TFT using MBED OS.

Unfortunately I will not show you a beautiful way to convert UTC to Eastern Time because I don’t actually know what that way would be which is very frustrating.  Every way that I know requires me to drag a lot of crap into my PSoC6 which I don’t really want to do.

For this article I will discuss:

  1. MBED OS Project Setup
  2. MBED OS Event Queues
  3. The RTOS Architecture
  4. The Display Function(s)
  5. The NTP Thread
  6. The WiFI/Main Thread
  7. The Whole Program

Project Setup: Create and Configure Project + Add the Libraries

You should start this project by creating a project by running “mbed new NTPDemo” or by using mbed studio to create a new project.  To run this project requires at least mbed-os-5.13.3.  You have two basic choices to get the latest mbed.  For some reason which I don’t totally understand when you run mbed new it gives you a slightly older version of mbed-os.  To get a new version you can either “cd mbed-os ; mbed update mbed-os-5.13.3” or to get the latest “cd mbed-os ; mbed update master”.

In this project I use three libraries, emWin, the IoT Expert ST7789V library (for the display) and the ntp-client library.  To add them run

The emWin library requires that you tell emWin about which version of the library .a to link with.  You can do this by adding this to the mbed_app.json

{
    "target_overrides": {
        "*": {
            "target.components_add": ["EMWIN_OSNTS"]
        }
    }
}

MBED OS Event Queues

For this project I will use one of the cool RTOS mechanisms that is built into MBED OS,  the “EventQueue“.   There is a nice tutorial in the MBED OS documentation.  An EventQueue is a tool for running a function “later” and in a different thread context.  What does that mean?  It means that there is a thread that sits around waiting until you tell it to run a function.  You tell it to run the function by pushing a function pointer into it’s EventQueue.  In other words, an EventQueue is a thread that waits for functions to be pushed into queue.  When the function is pushed into the queue it runs it.

How is this helpful?  There are a several of reasons.

  • If you are in an ISR it allows you to defer execution of something to the main program.
  • It can be used to serialize access to some resource – in my case the display.
  • It allows you to schedule some event to happen regularly

When MBED OS starts it automatically creates two of these event queues one of the queue threads runs at “osPriorityNormal” and can be accessed via mbed_event_queue();  The other event queue runs at “osPriorityHigh” and can be accesed by mbed_highprio_event_queue();  For some reason (which I don’t understand) these queues are documented on a separate page.

The RTOS Architecture

Here is a picture of the architecture of my program.

 

The Main Thread

The main function which is also the main thread, which then becomes the WiFi Thread

  1. Initializes the GUI
  2. Starts up the Display Event Queue
  3. Turns on the WiFi and attaches a callback (to notify the program of WiFI Status Changes)
  4. Try’s to connect to the WiFi Network
  5. If it fails, it updates the display and try’s again after 2 seconds
  6. Once it is connected it starts up the NTP Server Thread
  7. And then waits for the WiFi semaphore to be set… which only happens if WiFi gets disconnected at which point it goes back to the start of the WiFI connection and try again.
int main()
{
    int wifiConnectionAttempts;
    int ret;

    GUI_Init();
    displayQueue = mbed_event_queue();
    displayQueue->call_every(1000, &updateDisplayTime);

    wifi = WiFiInterface::get_default_instance();
    wifi->attach(&wifiStatusCallback);

   while(1)
   {
       wifiConnectionAttempts = 1;
        do {

            ret = wifi->connect(MBED_CONF_APP_WIFI_SSID, MBED_CONF_APP_WIFI_PASSWORD, NSAPI_SECURITY_WPA_WPA2);
            displayQueue->call(updateDisplayWiFiConnectAttempts,wifiConnectionAttempts);

            if (ret != 0) {
                wifiConnectionAttempts += 1;
                wait(2.0); // If for some reason it doesnt work wait 2s and try again
            }
        } while(ret !=0);

        // If the NTPThread is not running... then start it up
        if(netTimeThreadHandle.get_state() == Thread::Deleted)
            netTimeThreadHandle.start(NTPTimeThread);
 
        WiFiSemaphore.acquire();
   }

Display Event Queue

Display EventQueue is used to run functions which update the display.  By using an EventQueue it ensure that the Display updates happen serially and there are no display resource conflicts.  The four functions are

  1. updateDisplayWiFiStatus
  2. updateDisplayWifiConnectAttempts
  3. updateDisplayNTPCount
  4. updateDisplayTime

I wanted a function which could display the state of the WiFi connection on the screen.  This is a string of text which is generated in the connection status function.  In order to send the message, the connection status function will “malloc” which then requires the updateDisplayWiFiStatus function to free the memory associated with the message.

#define DISP_LEFTMARGIN 10
#define DISP_TOPMARGIN 4
#define DISP_LINESPACE 2
// updateDisplayWiFiStatus
// Used to display the wifi status
void updateDisplayWiFiStatus(char *status)
{
    GUI_SetFont(GUI_FONT_16_1);
    GUI_DispStringAt(status,DISP_LEFTMARGIN, DISP_TOPMARGIN); 
    free(status);  
}

When I started working on this program I had a bug in my connections so I added the ability to tell how many WiFI connections attempts had happened.  I also wondered how many times there might be a disconnect if I ran this program a long time.  The answer is I ran it for two days and it didn’t disconnect a single time.  This function simply takes a number from the caller and displays it on the screen.  Notice that I use snprintf to make sure that I don’t overrun the buffer (which I doubt could happen because I made it 128 bytes).

// updateDisplayWiFiConnectAttempts
// This function displays the number of attempted connections
void updateDisplayWiFiConnectAttempts(int count)
{
    char buffer[128];
    snprintf(buffer,sizeof(buffer),"WiFi Connect Attempts = %d",count); 
    GUI_SetFont(GUI_FONT_16_1);
    GUI_DispStringAt(buffer,DISP_LEFTMARGIN, DISP_TOPMARGIN + (GUI_GetFontSizeY()+DISP_LINESPACE) ); 
}

I was curious how many times the NTP connection would happen.  So I added the ability to display a count.  Notice that I use a static variable to keep track of the number of times this function is called rather than pushing the number as an argument.  Perhaps this is a design flaw?

// updateDisplayNTPCount
// updates the display with the number of time the NTP Server has been called
void updateDisplayNTPCount(void)
{
    static int count=0;
    char buffer[128];
    count = count + 1;
    snprintf(buffer,sizeof(buffer),"NTP Updates = %d\n",count);
    GUI_SetFont(GUI_FONT_16_1);
    GUI_DispStringHCenterAt(buffer,LCD_GetXSize()/2,LCD_GetYSize() - GUI_GetFontSizeY()); // near the bottom
}

The main display function is the seconds which is displayed in the middle of the screen.  I get the time from the RTC in the PSoC and is set by the NTP Server.  Notice my rather serious hack to handle the Eastern time difference to UTC… which unfortunately only works in the Summer.

// updateDisplayTime
// This function updates the time on the screen
void updateDisplayTime()
{
  time_t rawtime;
  struct tm * timeinfo;
  char buffer [128];

  time (&rawtime);
  rawtime = rawtime - (4*60*60); // UTC - 4hours ... serious hack which only works in summer

  timeinfo = localtime (&rawtime);
  strftime (buffer,sizeof(buffer),"%r",timeinfo);
  GUI_SetFont(GUI_FONT_32B_1);
  GUI_DispStringHCenterAt(buffer,LCD_GetXSize()/2,LCD_GetYSize()/2 - GUI_GetFontSizeY()/2);
}

NTP Time Thread

The Network Time Protocol was invented in 1981 by Dr. David Mills for use in getting Internet connected computers to have the right time.  Since then it has been expanded a bunch of times to include Cellular, GPS and other networks.  The scheme includes methods for dealing with propogation delay etc.  However, for our purposes we will just ask one of the NIST computers, what time is it?

The way it works is that you setup a structure with 48 bytes in it.  Then you open a UDP connection to an NTP server (which NIST runs for you) then it will fill out the same structure with some time data and send it back to you.  Here is the packet:

typedef struct
{

  uint8_t li_vn_mode;      // Eight bits. li, vn, and mode.
                           // li.   Two bits.   Leap indicator.
                           // vn.   Three bits. Version number of the protocol.
                           // mode. Three bits. Client will pick mode 3 for client.

  uint8_t stratum;         // Eight bits. Stratum level of the local clock.
  uint8_t poll;            // Eight bits. Maximum interval between successive messages.
  uint8_t precision;       // Eight bits. Precision of the local clock.

  uint32_t rootDelay;      // 32 bits. Total round trip delay time.
  uint32_t rootDispersion; // 32 bits. Max error aloud from primary clock source.
  uint32_t refId;          // 32 bits. Reference clock identifier.

  uint32_t refTm_s;        // 32 bits. Reference time-stamp seconds.
  uint32_t refTm_f;        // 32 bits. Reference time-stamp fraction of a second.

  uint32_t origTm_s;       // 32 bits. Originate time-stamp seconds.
  uint32_t origTm_f;       // 32 bits. Originate time-stamp fraction of a second.

  uint32_t rxTm_s;         // 32 bits. Received time-stamp seconds.
  uint32_t rxTm_f;         // 32 bits. Received time-stamp fraction of a second.

  uint32_t txTm_s;         // 32 bits and the most important field the client cares about. Transmit time-stamp seconds.
  uint32_t txTm_f;         // 32 bits. Transmit time-stamp fraction of a second.

} __PACKED ntp_packet_t;              // Total: 384 bits or 48 bytes.

The code to send the packet is really simple.  The only trick is that when you send data on the network you almost always use big endian, so you need to use the function nthol to convert.

void NTPClient::set_server(char* server, int port) {
    nist_server_address = server;
    nist_server_port = port;
}

time_t NTPClient::get_timestamp(int timeout) {
    const time_t TIME1970 = (time_t)2208988800UL;
    int ntp_send_values[12] = {0};
    int ntp_recv_values[12] = {0};

    SocketAddress nist;

    if (iface) {
        int ret_gethostbyname = iface->gethostbyname(nist_server_address, &nist);

        if (ret_gethostbyname < 0) {
            // Network error on DNS lookup
            return ret_gethostbyname;
        }

        nist.set_port(nist_server_port);

        memset(ntp_send_values, 0x00, sizeof(ntp_send_values));
        ntp_send_values[0] = '\x1b';

        memset(ntp_recv_values, 0x00, sizeof(ntp_recv_values));

        UDPSocket sock;
        sock.open(iface);
        sock.set_timeout(timeout);

        sock.sendto(nist, (void*)ntp_send_values, sizeof(ntp_send_values));

        SocketAddress source;
        const int n = sock.recvfrom(&source, (void*)ntp_recv_values, sizeof(ntp_recv_values));

        if (n > 10) {
            return ntohl(ntp_recv_values[10]) - TIME1970;

The times in the structure are represented with two 32-bit numbers

  • # of seconds since 1/1/1900 (notice this is not 1970)
  • # of fractional seconds in 1/2^32 chunks (that ain’t a whole lotta time)

The four numbers are

  • Reference Time – when you last sent a packet
  • Origin Time – when you sent the packet (from your clock)
  • Receive Time – when the NTP server received your packet
  • Transmit Time – when your NTP server sent the packet back to you

You know when you send the packet – called T1.  You know when you received the packet – called T4.  You know when the other side received your packet – called T2 and you know when the other side sent the packet called T3.  With this information you can calculate the network delay, stability of the clocks etc.  However, the simplest thing to do is to take the transit time, which is in UTC, and set your clock assuming 0 delay.

In MBEDOS to set the RTC clock in the PSoC you call the function with the number of seconds since 1/1/1970.  Don’t forget that the time that comes back from NTP is in seconds since 1/1/1900.

                set_time(timestamp);

Given that the PSoC 6 RTC counts in seconds you can just ignore the partial seconds.

WiFi Semaphore

At the top of main I registered to WiFi that I want a callback when the state of the WiFi changes.

    wifi->attach(&wifiStatusCallback);

This function does two things.

  • Updates the screen as the state goes from unconnected to connected
  • Unlocks a semaphore to tell the main thread to reconnect.
// wifiStatusCallback
// Changes the display when the wifi status is changed
void wifiStatusCallback(nsapi_event_t status, intptr_t param)
{
    const int buffSize=40;
    char *statusText;
    statusText = (char *)malloc(buffSize);

    switch(param) {
        case NSAPI_STATUS_LOCAL_UP:
            snprintf(statusText,buffSize,"WiFi IP = %s",wifi->get_ip_address());
            break;
        case NSAPI_STATUS_GLOBAL_UP:
            snprintf(statusText,buffSize,"WiFi IP = %s",wifi->get_ip_address());
            break;
        case NSAPI_STATUS_DISCONNECTED:
            WiFiSemaphore.release();
            snprintf(statusText,buffSize,"WiFi Disconnected");
            break;
        case NSAPI_STATUS_CONNECTING:
            snprintf(statusText,buffSize,"WiFi Connecting");
            break;
        default:
            snprintf(statusText,buffSize,"Not Supported");
            break;
    }
    displayQueue->call(updateDisplayWiFiStatus,statusText);
}

The Whole Program

Here is the whole program.

#include "mbed.h"
#include "GUI.h"
#include "mbed_events.h"
#include "ntp-client/NTPClient.h"

Thread netTimeThreadHandle;

WiFiInterface *wifi;
EventQueue *displayQueue;
Semaphore WiFiSemaphore;

/******************************************************************************************
*
* Display Functions
*
********************************************************************************************/

#define DISP_LEFTMARGIN 10
#define DISP_TOPMARGIN 4
#define DISP_LINESPACE 2
// updateDisplayWiFiStatus
// Used to display the wifi status
void updateDisplayWiFiStatus(char *status)
{
    GUI_SetFont(GUI_FONT_16_1);
    GUI_DispStringAt(status,DISP_LEFTMARGIN, DISP_TOPMARGIN); 
    free(status);  
}

// updateDisplayWiFiConnectAttempts
// This function displays the number of attempted connections
void updateDisplayWiFiConnectAttempts(int count)
{
    char buffer[128];
    snprintf(buffer,sizeof(buffer),"WiFi Connect Attempts = %d",count); 
    GUI_SetFont(GUI_FONT_16_1);
    GUI_DispStringAt(buffer,DISP_LEFTMARGIN, DISP_TOPMARGIN + (GUI_GetFontSizeY()+DISP_LINESPACE) ); 
}

// updateDisplayNTPCount
// updates the display with the number of time the NTP Server has been called
void updateDisplayNTPCount(void)
{
    static int count=0;
    char buffer[128];
    count = count + 1;
    snprintf(buffer,sizeof(buffer),"NTP Updates = %d\n",count);
    GUI_SetFont(GUI_FONT_16_1);
    GUI_DispStringHCenterAt(buffer,LCD_GetXSize()/2,LCD_GetYSize() - GUI_GetFontSizeY()); // near the bottom
}

// updateDisplayTime
// This function updates the time on the screen
void updateDisplayTime()
{
  time_t rawtime;
  struct tm * timeinfo;
  char buffer [128];

  time (&rawtime);
  rawtime = rawtime - (4*60*60); // UTC - 4hours ... serious hack which only works in summer

  timeinfo = localtime (&rawtime);
  strftime (buffer,sizeof(buffer),"%r",timeinfo);
  GUI_SetFont(GUI_FONT_32B_1);
  GUI_DispStringHCenterAt(buffer,LCD_GetXSize()/2,LCD_GetYSize()/2 - GUI_GetFontSizeY()/2);
}

/******************************************************************************************
* NTPTimeThread
* This thread calls the NTP Timeserver to get the UTC time
* It then updates the time in the RTC
* And it updates the display by adding an event to the display queue
********************************************************************************************/
void NTPTimeThread()
{
    NTPClient ntpclient(wifi);

    while(1)
    {
        if(wifi->get_connection_status() == NSAPI_STATUS_GLOBAL_UP)
        {
            time_t timestamp = ntpclient.get_timestamp();
            if (timestamp < 0) {
                // probably need to do something different here
            } 
            else 
            {
                set_time(timestamp);
                displayQueue->call(updateDisplayNTPCount);
            }
        }
        wait(60.0*5); // Goto the NTP server every 5 minutes
    }
}

/******************************************************************************************
*
* Main & WiFi Thread
*
********************************************************************************************/

// wifiStatusCallback
// Changes the display when the wifi status is changed
void wifiStatusCallback(nsapi_event_t status, intptr_t param)
{
    const int buffSize=40;
    char *statusText;
    statusText = (char *)malloc(buffSize);

    switch(param) {
        case NSAPI_STATUS_LOCAL_UP:
            snprintf(statusText,buffSize,"WiFi IP = %s",wifi->get_ip_address());
            break;
        case NSAPI_STATUS_GLOBAL_UP:
            snprintf(statusText,buffSize,"WiFi IP = %s",wifi->get_ip_address());
            break;
        case NSAPI_STATUS_DISCONNECTED:
            WiFiSemaphore.release();
            snprintf(statusText,buffSize,"WiFi Disconnected");
            break;
        case NSAPI_STATUS_CONNECTING:
            snprintf(statusText,buffSize,"WiFi Connecting");
            break;
        default:
            snprintf(statusText,buffSize,"Not Supported");
            break;
    }
    displayQueue->call(updateDisplayWiFiStatus,statusText);
}


int main()
{
    int wifiConnectionAttempts;
    int ret;

    GUI_Init();
    displayQueue = mbed_event_queue();
    displayQueue->call_every(1000, &updateDisplayTime);

    wifi = WiFiInterface::get_default_instance();
    wifi->attach(&wifiStatusCallback);

   while(1)
   {
       wifiConnectionAttempts = 1;
        do {

            ret = wifi->connect(MBED_CONF_APP_WIFI_SSID, MBED_CONF_APP_WIFI_PASSWORD, NSAPI_SECURITY_WPA_WPA2);
            displayQueue->call(updateDisplayWiFiConnectAttempts,wifiConnectionAttempts);

            if (ret != 0) {
                wifiConnectionAttempts += 1;
                wait(2.0); // If for some reason it doesnt work wait 2s and try again
            }
        } while(ret !=0);

        // If the NTPThread is not running... then start it up
        if(netTimeThreadHandle.get_state() == Thread::Deleted)
            netTimeThreadHandle.start(NTPTimeThread);
 
        WiFiSemaphore.acquire();
   }
}

 

MBEDOS Libraries & emWin Configuration Files

Summary

I have written a fair amount about Graphics Displays, using the Segger emWin graphics library and MBED OS.  I have found it irritating to do all of the configuration stuff required to get these kinds of projects going.  I inevitably go back, look at my old articles, find the github repository of my example projects etc.  This week I wanted to write some programs for the new CY8CKIT-062S2-43012 development kit so I thought that I would try all of the Cypress displays using that development kit.  Rather than starting with example projects, this time I decided to build configurable mbedos libraries. In this article I will show you how to build configurable mbed os libraries which will allow you to use the emWin Graphics Library with all of the Cypress display shields.

In this article I will walk you through:

  • The CY8CKIT-032 & SSD1306 Display Driver & emWin
  • MBED OS Libraries
  • MBED OS Configuration
  • Configuration Overrides
  • The SSD1306 emWin Configuration
  • Using the SSD1306 Configuration Library
  • emWin Configuration Libraries

The CY8CKIT-032 & SSD1306 Display Driver & emWin

The CY8CKIT-032 has a little 0.96″ OLED display that is attached to the Salomon Systech SSD1306 Display Driver.  I have written quite a bit about this little screen as it is cheap and fairly easy to use.  It became even easier when we released the Segger emWin SPAGE display driver.  And with my new library it should be trivial to use and configure for your setup.

You can read in detail about the functionality here but in short:

  • The display driver chip is attached to the PSoC via I2C
  • You need to provide the Segger emWin driver
    • GUIConfig.h/cpp – Segger GUI configuration
    • LCDConf.h/cpp – Setup files for the LCD
    • GUI_X_Mbed.cpp – RTOS control functions for delays etc.
    • ssd1306.h/c – physical interface to the SSD1306 controller
  • You need to initialize the PSoC I2C before talking to the display
  • You need to initialize the display driver chip before drawing on the screen

In general all of this configuration will be the same from project to project to project.  However, you may very will find that you have the display connected to a different set of pins.  I suppose that would put all of these files into some directory.  Then you could copy that directory into your project every time.  Which would leave you with modifying the configuration to meet your specific board connection.  The problem with that is you have now deeply intertwined your project with those files.

MBED OS has given us a really cool alternative.  Specifically the Library and configuration systems.

MBED OS Libraries

An MBED OS library is simply a git repository.  Just a directory of source files.  When you run the command “mbed add repository” it does two basic things

  1. It does a “git clone” to make a copy of the repository inside of your project.
  2. It creates a file with the repository name.lib which contains the URL to the version of the repository

Here is a an MBED add of my graphics configuration library for the SSD1306

(mbed CLI) ~/Mbed Programs/test032 $ mbed add git@github.com:iotexpert/mbed-os-emwin-ssd1306.git
[mbed] Working path "/Users/arh/Mbed Programs/test032" (program)
[mbed] Adding library "mbed-os-emwin-ssd1306" from "ssh://git@github.com/iotexpert/mbed-os-emwin-ssd1306" at latest revision in the current branch
[mbed] Updating reference "mbed-os-emwin-ssd1306" -> "https://github.com/iotexpert/mbed-os-emwin-ssd1306/#7986006c17bd572da317257640f35ec3b232414e"
(mbed CLI) ~/Mbed Programs/test032 $ ls mbed-os-emwin-ssd1306
GUIConf.cpp    GUI_X_Mbed.cpp LCDConf.h      mbed_lib.json  ssd1306.h
GUIConf.h      LCDConf.cpp    README.md      ssd1306.cpp
(mbed CLI) ~/Mbed Programs/test032 $ more mbed-os-emwin-ssd1306.lib 
https://github.com/iotexpert/mbed-os-emwin-ssd1306/#7986006c17bd572da317257640f35ec3b232414e
(mbed CLI) ~/Mbed Programs/test032 $ 

Notice that when I “ls’d” the directory that all of file required to confiugure emWin for the SSD1306 became part of my project.  And the file mbed-os-emwin-ssd1306.lib was created with the URL of the github repository.

https://github.com/iotexpert/mbed-os-emwin-ssd1306/#7986006c17bd572da317257640f35ec3b232414e

When you run “mbed compile” the build system just searches that directory for cpp and h files turns them into .0’s and add them to the the BUILD directory.  However, before it compiles it run the configuration system.

MBED OS Configuration System

The configuration system takes the file “mbed_lib.json” parses it and turns it into a C-header file called mbed_config.h.  The format of this file is

  • The name of the component – in this case “SSD1306_OLED”
  • The parameters of the component SDA, SCL, I2CADDRESS and I2CFREQ
{
    "name" : "SSD1306_OLED",
    "config": {
        "SDA":"P6_1",
        "SCL":"P6_0",
        "I2CADDRESS":"0x78",
        "I2CFREQ":"400000"
    }
}

This header file is then placed into the BUILD directory of your project and is included as part of #include “mbed.h”

If you open mbed_config.h you will find that it creates #defines of the component parameters

#define MBED_CONF_SSD1306_OLED_I2CADDRESS                                     0x78                                                                                             // set by library:SSD1306_OLED
#define MBED_CONF_SSD1306_OLED_I2CFREQ                                        400000                                                                                           // set by library:SSD1306_OLED
#define MBED_CONF_SSD1306_OLED_SCL                                            P6_0                                                                                             // set by library:SSD1306_OLED
#define MBED_CONF_SSD1306_OLED_SDA                                            P6_1

This is really nice because I can then reference those #defines in my source code.

Configuration Overrides

When you are building the library you can create an arbitrary number of these parameters which are then applied to all of the uses of that library.  Or if there is some reason why one target is different you can specify the parameters for that specific target by changing the mbed_lib.json.  For instance if the CY8CKIT_062S2_43012 need a 100K I2C frequency instead of 400K (it doesn’t), you could do this:

{
    "name" : "SSD1306_OLED",
    "config": {
        "SDA":"P6_1",
        "SCL":"P6_0",
        "I2CADDRESS":"0x78",
        "I2CFREQ":"400000"
    },
    "target_overrides" : {
        "CY8CKIT_062S2_43012" : {
            "I2CFREQ":"100000"
        }
    }
}

The application developer is also allowed to over-ride the parameter by providing the target overrides in the MBED OS file “mbed_app.json”.  Notice that the way you specify the parameter name is different in this file than the mbed_lib.json.  In this case you give it the name of the library.parametername.  Here is an example setting the I2CFrequency to 100K

{
	"target_overrides": {
        "*": {
            "target.components_add": ["EMWIN_OSNTS"]
        },
        "CY8CKIT_062S2_43012" : {
            "SSD1306_OLED.I2CFREQ": "1000000"
        }
	}
}

Which would result in a change to the generated #define in mbed_config.h

#define MBED_CONF_SSD1306_OLED_I2CFREQ                                        1000000

Notice that you can specify a “*” to match all of the targets, or you can specify the exact target.

The SSD1306 emWin Configuration

I use the configuration system to generate #defines for the

  • SCL/SDA Pin Numbers
  • I2C Address
  • I2C Frequency

Which lets my use those #defines in ssd1306.cpp

I2C Display_I2C(MBED_CONF_SSD1306_OLED_SDA, MBED_CONF_SSD1306_OLED_SCL);

And

void ssd1306_Init(void) 
{
    Display_I2C.frequency(MBED_CONF_SSD1306_OLED_I2CFREQ);
}

Using the SSD1306 Configuration Library

Lets make an example project that uses the CY8CKIT_062S2_43012 and the CY8CKIT032 using the Segger graphics library and my configuration library.

Start by make a new project, adding the emWin library and the configuration library.  It should look something like this

Now edit the mbed_app.json to add the emWin library

{
	"target_overrides": {
        "*": {
            "target.components_add": ["EMWIN_OSNTS"]
        }
	}
}

Create the main.cpp which simply initializes the display and displays “hello world”

#include "mbed.h"
#include "GUI.h"

int main()
{
  GUI_Init();
  GUI_SetColor(GUI_WHITE);
  GUI_SetBkColor(GUI_BLACK);
  GUI_SetFont(GUI_FONT_13B_1);
  GUI_SetTextAlign(GUI_TA_CENTER);
  GUI_DispStringAt("Hello World", GUI_GetScreenSizeX()/2,GUI_GetScreenSizeY()/2 - GUI_GetFontSizeY()/2);
}

When you compile it with

  • mbed compile -t GCC_ARM -m CY8CKIT_062S2_43012 -f

You should get something like this:

And your screen should look like this (notice I made the font bigger than the original screen shot)

emWin Configuration Libraries

Finally I created libraries for all of the Cypress displays.  You can use these to make your project easier to get going.

 

Debugging SSD1306 Display Problems

Summary

This article explains in detail how to use and debug SSD1306 displays.  In this article, I use the Segger emWin library and MBEDOS, but for all practical purposes this discussion applies to all other interfaces to the board including Arduino, Raspberry Pi, Adafruit, etc.  I will say from the outset that I spent far far too much time digging into the inner workings of an 11 year old graphics driver.  Oh well, hopefully someone will get some benefit.

A year ago (or so) I designed a user interface board called the CY8CKIT-032 to go with my Cypress WICED WiFi book and class.  This board uses a PSoC 4 Analog co-processor which can do a bunch of cool stuff.  I have a series of articles planned about that board, but that will be for another day.  One of the things that I did was put a 0.96″ I2C OLED Display based on a SSD1306 driver on the board.  These displays are widely available from Alibaba and eBay for <$2.  I think that the displays are being used in inexpensive cells phones in China so there are tons of them and they are CHEAP!  The bad news is that if you google “ssd1306 problems” you will find an absolute rogues gallery of unpleasantness.  It seems that tons of people struggle to get these things working.

This whole thing started last week as Cypress released and update to our MBED OS implementation.  This update included releasing a complete set of the Segger emWin drivers.  I had been wanting to step up to a more robust graphics library than the Adafruit library that I used in this article.  I was pleased to see that our release included the emWin SPAGE driver which knows how to talk to a bunch of different page based displays including the SSD1306.

But, as always, I had to wrestle with the display a little bit before I got everything working.  This time I wrote down what I did/learned.  So, for this article I will describe

  • The SSD1306 Electrical Interface
  • The SSD1306 Software Interface
  • The SSD1306 Driver Registers
  • The SSD1306 Graphics Data RAM
  • Reading from the Frame Buffer
  • My Initialization Sequence
  • Some Other Initialization Sequences
  • A Bunch of Screen Problems & How To Fix

The Electrical Interface

There is not a lot to know about the electrical interface.  The data sheet specifies that the device can use I2C, SPI, 6800 and 8080.  I have not seen either the 6800 or 8080 interface put onto any of these OLED displays.  Like all driver chips, the SSD1306 has an absolute boatload of pins, in fact, 281.  The chip is long and skinny and was made to be mounted either on the display under the glass or on the flex connector.  Of the 281 pins, 128+64=196 are connected to the segments and commons in the display.  The rest of the pins are either capacitors, no-connects, power/ground or data signals.  The data signals are

  • D0-D7 either parallel data for 8080/6800 or SDA/SCL for I2C or MOSI/MISO for SPI
  • E – enable signal for 6800 or RD for 8080
  • R/W# – Read Write for 6800/8080
  • CS – Chip Select for SPI, 8080, 6800
  • D/C# – Data or Command for SPI, 6800, 8080 or Slave Address Select for I2C
  • Reset – Chip reset

For the I2C configurations it is common to tie the reset pin High and not bring the pin to a connector.  The SA0 is also typically connected via a 0-ohm resistor to either 0 or 1 which configures the device to have the 7-bit address 0x3C or 0x3D or 8-bit 0x78 or 0x7A.  Here is a picture of the back of one of my boards where you can see the 0ohm resistor.

Sometimes all of the data pins are available on the back of the board.  This lets you move/add/change the 0-ohm resistors to configure the mode of the chip.

One thing you should be careful about is the I2C connections.  I looked around on eBay and Alibaba to find a few pictures of the I2C displays.  You should notice that all three of these displays are I2C, but all three of them have a different position and ORDER of VCC/GND/SCL/SDL  When we ordered displays from China to go onto the CY8CKIT-032 we found displays in the same BATCH that had different orders of the VCC/GND.

   

Here is a SPI version that has reset and data/command pin brought out.

 

The Software Interface

There are two parts to the software interface.

The first part is the command interface.  Inside of the chip there are a bunch of logic circuits which which configure the charge pumps, sequence COMs and SEGs, charge and discharge capacitors etc.  All of these things are configurable to allow for different configurations of screens e.g. different x-y sizes, configuration of what wires are connected to what places on the glass etc.  Before you can get the display to work correctly you must initialize all of these values by sending commands.  All the commands are 1-byte followed by 0 or more command parameters.

The second part is the data interface.  Inside of the SSD1306 chip there is a Graphics Display DRAM – GDDRAM which has 1 bit for every pixel on the screen. The state machine inside of the chip called the Display Controller will loop through the bits one by one and display them on the correct place on the screen.  This means that your MCU does not need to do anything to keep the display up to date.  When you want a pixel lit up on the screen you just need to write the correct location in the GDDRAM.

For the rest of this article I will focus on the serial interface, I2C.  How do you send commands and data?  Simple.  When you start a transaction you send a control byte which tells the controller what to expect next.  There are four legal control bytes.

  • 0b10000000 = 0x80 = multiple commands
  • 0b00000000 = 0x00 = one command
  • 0b11000000 = 0xC0 = multiple data
  • 0b01000000 = 0x40 = one data byte

Here is the picture from the datasheet (which I don’t find particularly illuminating) but it does describe the control byte.

To send commands you write to the I2C bus with a control byte, then you send the command, then you send the optional parameters.  If you want to send multiple commands you send the control byte 0x80, the command + parameters as many as you need.

The SSD1306 Driver Registers

In order for the driver chip to drive the screen you need to configure:

  1. How the driver is electrically connected to the OLED Screen
  2. What are the electrical parameters of the screen
  3. What are the electrical parameters of the system
  4. How you want to address the frame buffer
  5. The automatic scroll configuration settings
  6. The pixel data for the frame buffer, though it will happily display noise.

If you bought this screen from eBay, Adafruit, Alibaba etc. then you will get no say in 1-3, the electrical parameters of the system.  Your screen will come prewired with all of the capacitors, OLED etc already attached to your driver commons and segments.  If you didn’t buy the screen prepackaged, then it is highly unlikely you are reading this article.  What this means is that you need to know the initializing sequence required to get the screen to work properly, then you just send the sequence down the wire from your MCU to the screen.  From looking around on the internet, it appears to me that there in only one parameter that is different in any of the screens that I could find.  Specifically the number of lines on the screen – either 32 or 64.  Which means that all of these initialization implementations should really on have one difference register 0xA8 should be set to either n-1 aka 31 or 63

The other difference that you will see between different implementations is the memory address mode.  In other words, how do you want to write data into the frame buffer from the MCU.  Many of the open source graphics libraries use “Horizontal” mode.  The Segger emWin library that I am using uses “Page” mode.  More on this later.

When you look in the data sheet, unfortunately they mix and match the order of the information.  However, from the data sheet, the categories are:

  1. Fundamental Commands
  2. Scrolling Commands
  3. Address Setting Commands
  4. Hardware Configuration
  5. Timing and Driving Scheme
  6. Charge Pump

I won’t put screen shots of the whole data sheet into this article, but I will show the command table and make a few clarifications on the text.  Or at least I will clarify places where I got confused.

As to the fundamental commands.  I tried a bunch of different contrast settings on my screens and could not tell the difference between them.  I tried from 0x10 to 0xFF and they all looked the same to me.  The best course of action is to use the default 0x7F.  I don’t really know why there is a command 0xA5 “Entire Display ON ignore RAM”.  The data sheet says “A5h command forces the entire display to be “ON”, regardless of the contents of the display data RAM”.  I can’t think of a single use case for this.  I suppose that if you issue 0xAE the screen will be all black… and if you issue 0xA5 the screen will be all white?  But why?

And my definitions in the C driver file:

////////////////////////////////////////////////////////////////////////
// Fundamental Command Table Page 28
////////////////////////////////////////////////////////////////////////
#define OLED_SETCONTRAST                              0x81
// 0x81 + 0-0xFF Contrast ... reset = 0x7F

// A4/A5 commands to resume displaying data
// A4 = Resume to RAM content display
// A5 = Ignore RAM content (but why?)
#define OLED_DISPLAYALLONRESUME                       0xA4
#define OLED_DISPLAYALLONIGNORE                       0xA5

// 0xA6/A7 Normal 1=white 0=black Inverse 0=white  1=black
#define OLED_DISPLAYNORMAL                            0xA6
#define OLED_DISPLAYINVERT                            0xA7

// 0xAE/AF are a pair to turn screen off/on
#define OLED_DISPLAYOFF                               0xAE
#define OLED_DISPLAYON                                0xAF

In the next section of the command table are the “Scrolling” commands.  It appears that this graphics chip was setup to display text that is 8-pixels high.  The scrolling commands will let you move the screen up/down and left/right to scroll automatically without having to update the the frame buffer.  In other words it can efficiently scroll the screen without a bunch of load on your MCU CPU or on the data bus between them.  The Adafruit graphics library provides the scrolling commands.  However, I am not using them with the Segger Library.

The next section has the commands to configure how your MCU writes data into the Graphics RAM aka the frame buffer. These commands fall into two categories.  First the address mode.  The address modes help you efficiently write the GDDRAM.  When you send data to the frame buffer you really don’t want to send

  • address, pixel, address, pixel, …

What you really would like to do is send

  • Address, pixel, pixel, pixel … (and have the address be automatically incremented

At first blush you might think… why do I need a mode?  Well there are some people who want the x address incremented… there are some people who want the y-address incremented and there are some people who want to have page address access.  And what do you do when you get to the end of a line? or a column or a page? and what does the end mean?

The second set of commands in this table are the commands to set the starting address before you write data.

  

////////////////////////////////////////////////////////////////////////
// Address Setting Command Table
////////////////////////////////////////////////////////////////////////

// 00-0F - set lower nibble of page address
// 10-1F - set upper niddle of page address

#define OLED_SETMEMORYMODE                            0x20
#define OLED_SETMEMORYMODE_HORIZONTAL                 0x00
#define OLED_SETMEMORYMODE_VERTICAL                   0x01
#define OLED_SETMEMORYMODE_PAGE                       0x02

// 0x20 + 00 = horizontal, 01 = vertical 2= page >=3=illegal

// Only used for horizonal and vertical address modes
#define OLED_SETCOLUMNADDR                            0x21
// 2 byte Parameter
// 0-127 column start address 
// 0-127 column end address

#define OLED_SETPAGEADDR                              0x22
// 2 byte parameter
// 0-7 page start address
// 0-7 page end Address

// 0xB0 -0xB7 ..... Pick page 0-7

The hardware configuration registers allow the LED display maker to hookup the common and segment signals in an order that makes sense for the placement of the chip on the OLED glass.  For a 128×64 display there are at least 196 wires, so the routing of these wires may be a total pain in the ass depending on the location of the chip.  For instance the left and right might be swapped… or half the wires might come out on one side and the other half on the other side.  These registers allow the board designer flexibility in making these connections.  Commands 0xA0, 0xA1, 0xA8, 0xC0, 0xC8, 0xD3, 0xDa will all be fixed based on the layout.  You have no control and they need to be set correctly or something crazy will come out.

////////////////////////////////////////////////////////////////////////
// Hardware Configuration
////////////////////////////////////////////////////////////////////////

// 40-7F - set address startline from 0-127 (6-bits)
#define OLED_SETSTARTLINE_ZERO                        0x40

// Y Direction
#define OLED_SEGREMAPNORMAL                           0xA0
#define OLED_SEGREMAPINV                              0xA1

#define OLED_SETMULTIPLEX                             0xA8
// 0xA8, number of rows -1 ... e.g. 0xA8, 63

// X Direction
#define OLED_COMSCANINC                               0xC0
#define OLED_COMSCANDEC                               0xC8

// double byte with image wrap ...probably should be 0
#define OLED_SETDISPLAYOFFSET                         0xD3

// Double Byte Hardware com pins configuration
#define OLED_SETCOMPINS                               0xDA
// legal values 0x02, 0x12, 0x022, 0x032

The next sections of commands are part of the electrical configuration for the glass.

0xD5 essentially sets up the display update rate by 1) setting the display update clock frequency and 2) setting up a divider for that clock.

0xDB and 0xD9 sets up a parameter that is display dependent.  That being said I tried a bunch of different values and they all look the same to me.

////////////////////////////////////////////////////////////////////////
// Timing and Driving Scheme Settings
////////////////////////////////////////////////////////////////////////

#define OLED_SETDISPLAYCLOCKDIV                       0xD5
#define OLED_SETPRECHARGE                             0xD9

#define OLED_SETVCOMDESELECT                          0xDB
#define OLED_NOP                                      0xE3

These displays require a high voltage to program the liquid crystal in the display.  That voltage can either be supplied by an external pin or by an internal charge pump.  All the displays that I have seen use an internal charge pump.

////////////////////////////////////////////////////////////////////////
// Charge Pump Regulator
////////////////////////////////////////////////////////////////////////

#define OLED_CHARGEPUMP                               0x8D
#define OLED_CHARGEPUMP_ON                            0x14
#define OLED_CHARGEPUMP_OFF                           0x10

The SSD1306 Graphics Data RAM

In order to actually get data to display on the screen you need to write 1’s and 0’s into the Graphics Data RAM that represents your image.  The memory is actually organized into 8 pages that are each 128 bits wide and 8 bits tall.  This means that if you write 0b10101010 to location (0,0) you will get the first 8 pixels in a column on the screen to be on,off,on,off,on,off,on,off.  Notice that I said vertical column and not row.  Here is a picture from the data sheet.  That shows the pages:

And then they show you in the data sheet that the pixels go down from the first row of the page.

In order to make the writing process easier and lower bandwidth the SSD1306 has three automatic addressing modes.

  • Horizontal – Set the page address start, end and the column start and end… bytes write 8 vertical pixels on the page. Each byte write advances the column until it wraps to the next page and resets the column to the “start”
  • Vertical – Set the page address start, end and the column start and end… bytes write 8 vertical pixels on the page.  Each byte write advances the page until it wraps vertically where it increments the column and resets the page back to the start page.
  • Page – Set the page address and column start/end.  Each byte writes vertically.  Wraps back onto the same page when it hits the end column.

In Horizontal and Vertical mode you

  • Set the range of columns that you want to write (using 0x22)
  • Set the range of pages you want to write (using 0x21)
  • Write bytes

In the page mode you

  • Set the page (remember you can only write one page at a time in page mode) using 0xB0-0xB7
  • Set the start column using 0x0? and 0x1?

Here is a picture from the data sheet of horizontal address mode:

In this bit of example code I am saying to iterate through the pages 0->7… in other words all of the pages.  And to start in column 0.  This example will make 12 columns of pixels each 8 high starting a (0,0) on the screen…

    char horizontalExample[]= {
            0xAE,
            0x20, /// address mode
            0x00, // Horizontal
            0xA4,
            0xAF,
            0x22, //Set page address range
            0,
            7,
            0x21, // column start and end address
            0,
            127,
    };

    I2C_WriteCmdStream(horizontalExample, sizeof(horizontalExample));
    // Write twelve bytes onto screen with 0b10101010
    for(int i=0;i<12;i++)
        I2C_WriteData(0xAA);

Here is a picture of what it does.

Here is a picture from the data sheet of vertical address mode:

This example code sets the page range to 0–>7  (the whole screen) and the column range 0–>127 (the whole screen).  Then writes 12 bytes.  You can see it wrap at the bottom and move back to page 0 column 1.

    char verticalExample[]= {
            0xAE,
            0x20, /// address mode
            0x01, //  vertical
            0xA4,
            0xAF,
            0x22, //Set page address range
            0,
            7,
            0x21, // column start and end address
            0,
            127,
    };

    I2C_WriteCmdStream(verticalExample, sizeof(verticalExample));
    // Write twelve bytes onto screen with 0b10101010
    for(int i=0;i<12;i++)
        I2C_WriteData(0xAA); 

 

 

In page mode you just set the page and the start and end column.  0xB0 means page 0, 0xB1 means page 1… 0xB7 means page 7.

You can see that I started from column 0x78 (meaning column 120) and that it wraps back to column 0 on the SAME page.

    char pageExample[]= {
            0xAE,
            0x20, // address mode
            0x02, // Page mode
            0xA4, // Resume from ram
            0xAF, // Screen on
            0xB0, // Start from page 0
            // Start from column 0x78 aka 120
            0x08, // Column lower nibble address
            0x17  // Column upper nibble address
    };

    I2C_WriteCmdStream(pageExample, sizeof(pageExample));

    // Write twelve bytes onto screen with 0b10101010
    for(int i=0;i<12;i++)
        I2C_WriteData(0xAA);

Here is what it looks like.

Reading from the Frame Buffer

Now that you know how to write to the Frame Buffer, the next question is how do you read?  For instance if you want to turn on 1 pixel (of a byte) but leave the others alone can you do this? The answer is NO.  In serial mode the device only writes.  In all of the Graphics libraries that I have seen they handle this by having a Frame Buffer in the MCU as well.  Duplicated resources… oh well.

My Initialization Sequence

I have a function that writes an array of bytes to the command registers.  So for me to initialize the screen I just need to set up that array.  Here is my best known setup.

    const char initializeCmds[]={
        //////// Fundamental Commands
        OLED_DISPLAYOFF,          // 0xAE Screen Off
        OLED_SETCONTRAST,         // 0x81 Set contrast control
        0x7F,                     // 0-FF ... default half way

        OLED_DISPLAYNORMAL,       // 0xA6, //Set normal display 

        //////// Scrolling Commands
        OLED_DEACTIVATE_SCROLL,   // Deactive scroll

        //////// Addressing Commands
        OLED_SETMEMORYMODE,       // 0x20, //Set memory address mode
        OLED_SETMEMORYMODE_PAGE,  // Page

        //////// Hardware Configuration Commands
        OLED_SEGREMAPINV,         // 0xA1, //Set segment re-map 
        OLED_SETMULTIPLEX,        // 0xA8 Set multiplex ratio
        0x3F,                     // Vertical Size - 1
        OLED_COMSCANDEC,          // 0xC0 Set COM output scan direction
        OLED_SETDISPLAYOFFSET,    // 0xD3 Set Display Offset
        0x00,                     //
        OLED_SETCOMPINS,          // 0xDA Set COM pins hardware configuration
        0x12,                     // Alternate com config & disable com left/right
   
        //////// Timing and Driving Settings
        OLED_SETDISPLAYCLOCKDIV,  // 0xD5 Set display oscillator frequency 0-0xF /clock divide ratio 0-0xF
        0x80,                     // Default value
        OLED_SETPRECHARGE,        // 0xD9 Set pre-changed period
        0x22,                     // Default 0x22
        OLED_SETVCOMDESELECT,     // 0xDB, //Set VCOMH Deselected level
        0x20,                     // Default 

        //////// Charge pump regulator
        OLED_CHARGEPUMP,          // 0x8D Set charge pump
        OLED_CHARGEPUMP_ON,       // 0x14 VCC generated by internal DC/DC circuit

        // Turn the screen back on...       
        OLED_DISPLAYALLONRESUME,  // 0xA4, //Set entire display on/off
        OLED_DISPLAYON,           // 0xAF  //Set display on
    };

Some Other Initialization Sequences

If you look around you will find many different SSD1306 libraries.  You can run this search on github.

Here is one example from https://github.com/vadzimyatskevich/SSD1306/blob/master/src/ssd1306.c  This is pretty much the same as mine except that the author put them in some other order than the data sheet.  I am not a huge fan of “ssd1306Command( SSD1306_SEGREMAP | 0x1)” but it does work.

void  ssd1306Init(uint8_t vccstate)
{
  _font = (FONT_INFO*)&ubuntuMono_24ptFontInfo;
  
    // Initialisation sequence
    ssd1306TurnOff();
    //  1. set mux ratio
    ssd1306Command(   SSD1306_SETMULTIPLEX );
    ssd1306Command(   0x3F );
    //  2. set display offset
    ssd1306Command(   SSD1306_SETDISPLAYOFFSET );
    ssd1306Command(   0x0 );
    //  3. set display start line
    ssd1306Command(   SSD1306_SETSTARTLINE | 0x0 ); 
    ssd1306Command( SSD1306_MEMORYMODE);                    // 0x20
    ssd1306Command( 0x00);                                  // 0x0 act like ks0108
    //  4. set Segment re-map A0h/A1h    
    ssd1306Command(   SSD1306_SEGREMAP | 0x1);
    //   5. Set COM Output Scan Direction C0h/C8h
    ssd1306Command(   SSD1306_COMSCANDEC);
    //  6. Set COM Pins hardware configuration DAh, 12
    ssd1306Command(   SSD1306_SETCOMPINS);
    ssd1306Command(   0x12);
    //  7. Set Contrast Control 81h, 7Fh
    ssd1306Command(   SSD1306_SETCONTRAST );
    if (vccstate == SSD1306_EXTERNALVCC) { 
        ssd1306Command(   0x9F );
    } else { 
        ssd1306Command(   0xff );
    }
    //  8. Disable Entire Display On A4h
    ssd1306Command(   SSD1306_DISPLAYALLON_RESUME);
    //  9. Set Normal Display A6h 
    ssd1306Command(   SSD1306_NORMALDISPLAY);
    //  10. Set Osc Frequency  D5h, 80h 
    ssd1306Command(   SSD1306_SETDISPLAYCLOCKDIV);
    ssd1306Command(   0x80);
    //  11. Enable charge pump regulator 8Dh, 14h 
    ssd1306Command(   SSD1306_CHARGEPUMP );
    if (vccstate == SSD1306_EXTERNALVCC) { 
        ssd1306Command(   0x10);
    } else { 
        ssd1306Command(   0x14);
    }
    //  12. Display On AFh 
    ssd1306TurnOn();

}

Here is another example from git@github.com:lexus2k/ssd1306.git

https://github.com/lexus2k/ssd1306/blob/master/src/lcd/oled_ssd1306.c

Honestly if I had found this originally I would not have gone to all the trouble.

static const uint8_t PROGMEM s_oled128x64_initData[] =
{
#ifdef SDL_EMULATION
    SDL_LCD_SSD1306,
    0x00,
#endif
    SSD1306_DISPLAYOFF, // display off
    SSD1306_MEMORYMODE, HORIZONTAL_ADDRESSING_MODE, // Page Addressing mode
    SSD1306_COMSCANDEC,             // Scan from 127 to 0 (Reverse scan)
    SSD1306_SETSTARTLINE | 0x00,    // First line to start scanning from
    SSD1306_SETCONTRAST, 0x7F,      // contast value to 0x7F according to datasheet
    SSD1306_SEGREMAP | 0x01,        // Use reverse mapping. 0x00 - is normal mapping
    SSD1306_NORMALDISPLAY,
    SSD1306_SETMULTIPLEX, 63,       // Reset to default MUX. See datasheet
    SSD1306_SETDISPLAYOFFSET, 0x00, // no offset
    SSD1306_SETDISPLAYCLOCKDIV, 0x80,// set to default ratio/osc frequency
    SSD1306_SETPRECHARGE, 0x22,     // switch precharge to 0x22 // 0xF1
    SSD1306_SETCOMPINS, 0x12,       // set divide ratio
    SSD1306_SETVCOMDETECT, 0x20,    // vcom deselect to 0x20 // 0x40
    SSD1306_CHARGEPUMP, 0x14,       // Enable charge pump
    SSD1306_DISPLAYALLON_RESUME,
    SSD1306_DISPLAYON,
};

Debug: Test the Hardware

If a your screen is not working, the first thing to do is use a multimeter and make sure that VCC=SCL=SDA=3.3V.  (in the picture below my camera caught the screen refresh partially through… It looks fine at normal speed).  I have the red probe attached to the SCL.

I would then run the bridge control panel and make sure that the device is responding.  You can do this by pressing “List”.  In the picture below you can see that there are two devices attached to the bus,  my screen is set to 0x78/0x3C.

If you don’t have the bridge control panel then you can implement I2Cdetect using your development kit.   Read about it here.

The next thing to do is attach a logic analyzer and make sure that the startup commands are coming out of your MCU correctly.  Notice that the 00, 0xAE, 0x81… are exactly the configuration sequence that I wrote in the driver code above.

Debug: Test the Firmware

If your screen is still not working here are some problems and what to do about them.

  • Speckled Screen
  • Solid Screen
  • Screen Flipped in the Y direction
  • Screen Flipped in the X Direction
  • Screen Flipped in both Directions
  • Screen is Inverted
  • Image is Partially off the Screen
  • Image is Wrapped on the Screen
  • Black Screen
  • Screen Has Gone Crazy

Speckled Screen

If you have the speckled screen this means that your screen is displaying an uninitialized frame buffer which the SSD people call the GDDRAM.  These are basically the random 0 and 1s that are the startup values in the SSD1306.  If this is happening then your graphic data is probably not being transferred between your MCU and the SSD1306.  This almost certainly means you have a problem in your porting layer.

Speckled Screen

If your screen is solid white that probably means you turned the screen back on without resuming from the graphics ram.  You did this:

OLED_DISPLAYALLONIGNORE,  // 0xA5, //Set entire display on/off

instead of this:

OLED_DISPLAYALLONRESUME,  // 0xA4, //Set entire display on/off

Screen Flipped in the Y direction

The commands C0/C8 set the direction in which the com lines are scanned.  Either from top to bottom or bottom to top.  Change C0–>C8 to the other way.
#define OLED_COMSCANINC                               0xC0
#define OLED_COMSCANDEC                               0xC8

Screen Flipped in the X Direction

In the X-Direction the A0/A1 set the configuration of scanning.  Try using A0–>A8 or the other way.

// X Direction Scanning 
#define OLED_SEGREMAPNORMAL                           0xA0
#define OLED_SEGREMAPINV                              0xA1

Screen Flipped in both Directions

If it is flipped in both X and Y direction then flip both of the direction registers.

// Y Direction
#define OLED_SEGREMAPNORMAL                           0xA0
#define OLED_SEGREMAPINV                              0xA1

// X Direction
#define OLED_COMSCANINC                               0xC0
#define OLED_COMSCANDEC                               0xC8

Screen is Inverted

If your screen is inverted then try A8–>A6

#define OLED_DISPLAYNORMAL                            0xA6
#define OLED_DISPLAYINVERT                            0xA7

Image is Partially off the Screen

If your image is off the screen the you probably have the wrong value for MULTIPLEX.

#define OLED_SETMULTIPLEX                             0xA8

The parameter is supposed to be the number of lines on the screen -1.  In my case the screen is 128×64 so my valued should be 63 aka 0x3F

        OLED_SETMULTIPLEX,        // 0xA8 Set multiplex ratio
        0x3F,                     // Vertical Size - 1

Image is Wrapped on the Screen

// Double byte CMD image wrap ...probably should be 0
#define OLED_SETDISPLAYOFFSET                         0xD3

The offset value allows the board designer to hook up the rows in a crazy fashion.   My screen has the top row to the top row number.

        OLED_SETDISPLAYOFFSET,    // 0xD3 Set Display Offset
        0x00,                     //

\

Black Screen

If you screen is totally dead…

Then the charge pump may be off

        //////// Charge pump regulator
        OLED_CHARGEPUMP,          // 0x8D Set charge pump
        0x14,                     // VCC generated by internal DC/DC circuit

or maybe the screen is off… try turning it on.

        OLED_DISPLAYON,           // 0xAF  //Set display on

or maybe you haven’t displayed anything. The screen is off trying sending a screen invert

#define OLED_DISPLAYINVERT                            0xA7

The Screen Has Gone Crazy

The register 0xDA SetComPins register will make some crazy results of it isn’t set correctly.  For my 0.96″ inch screen it needs to be set to 0x12

// Double Byte Hardware com pins configuration
#define OLED_SETCOMPINS                               0xDA
// legal values 0x02, 0x12, 0x022, 0x032

This is what happens with 0x02 [If you see the note below from Ivan, 0x02 is apparently for 128×32 and this screen is 128×64=0x12]

And 0x22

Finally 0x32

This was absolutely the craziest rabbit hole that I have ventured down. Nicholas has talked to me 10 times about doing this and he thinks I’m crazy.  Oh well.

MBEDOS Little File System & CY8CPROTO_62_4343W

Summary

This is the first article in a series that will discuss how to use the MBED OS file systems with Cypress SPI Nor Flash chips and PSoC 6.

Title
The Back Story & Making the LittleFS Work with the CY8CKIT_062_4343W
The Architecture of Filesystems in MBEDOS
SPI Nor Flash
SFDP
The MBED OS Quad SPI Driver
LittleFS
FATFS
MBED OS and POSIX Files

 

The Back Story

On a bunch of our development kits there is a SPI NOR Flash sitting right next to the PSoC 6.  Which exact SPI flash depends on the exact generation of development kit.  I have always wanted to use these chips, but had never had time to sort out how they work.  And quite frankly we never made it very easy to use them because although they were connected, we didn’t provide much in the way of software support.  However, with the advent of MBED OS at Cypress we were suddenly gifted with two file systems to use, LittleFS and FATFS.

This journey starts with an email note to the Applications manager in India (an awesome woman named Jaya)… “Hey, can you get someone to send me an example of the MBED OS flash file system on the CY8CPROTO_062_4343W.”  A day or so later I got an email with an attached project and a “memo” that explained what to do.  This exchange happened right before Embedded World in February and I was really busy.  Finally, a couple of weeks ago I read the email and the instructions which started with “Break off the NOR Flash wing and solder….”  If you look in the picture below you can see that at the top of the kit there is a breakaway wing (circled in green) that has a SPI Flash chip on it (circled in red).

Honestly, I didn’t read any further than “.. break off the wing…”.  So, I sent another note … “Uh… how about no.  Why can’t I use the development kit without soldering?”… And these two emails were my first steps down the Embedded FileSystem & NOR Flash Rabbit Hole which is the subject of this series of articles.

Making the LittleFS Work with the CY8CKIT_062_4343W

I am going to start by giving you the step by step instructions to make the LittleFS work … and these instruction will only include a little bit of commentary on how it works.  I will expand on the “how” in all of the follow on articles.  To make it work you need to follow these steps:

  1. Clone the MBEDOS FileSystem Example
  2. Clone my QSPI driver path
  3. Then patch MBEDOS with the updated QSPI driver.
  4. Test
  5. Examine the Project

The first step in the process of running the example is to clone the MBED OS Example Project for Filesystems.  To do this, run “mbed import mbed-os-example-filesystem”.  As I noted above, the default MBED does not have the required drivers for the Quad SPI interface.  Fortunately another excellent Applications engineer in India named Vaira built me a QSPI driver in advance of the actual official release from Cypress.  I have put these drivers on the iotexpert github repository and you can get them with a “git clone git@github.com:iotexpert/MBED_QSPI_PATCHES.git”.  Once you have them you can apply the patch by

  1. cd mbed-os-example-filesystem
  2. ../MBED_QSPI_PATCHES/patch-qspi-mbed.sh

The shell script is simple program that copies the driver files into the correct locations in your mbed-os directory in your current project.  I will talk in detail about these files in a later article.

#!/bin/sh

cp ../MBED_QSPI_PATCHES/qspi_api.c ../MBED_QSPI_PATCHES/objects.h mbed-os/targets/TARGET_Cypress/TARGET_PSoC6
cp ../MBED_QSPI_PATCHES/targets.json mbed-os/targets
cp ../MBED_QSPI_PATCHES/PinNames.h mbed-os/targets/TARGET_Cypress/TARGET_PSOC6/TARGET_CY8CMOD_062_4343W/TARGET_CY8CPROTO_062_4343W/

Here is what my terminal looks like after I run the import, clone and apply patches.

Next I will build the project “as-is” using “mbed compile -t GCC_ARM -m CY8CPROTO_062_4343W”

OK, the project looks like it builds with no problems (other than a very annoying boatload of warnings – I really wish people weren’t slobs).  Running the compile also has the nice side effect of setting the default target and toolchain.  You can see this by either looking at the “.mbed” file or by running “mbed config target” or “mbed config toolchain”.  Here is what my terminal window looks like

Test

I generally like to test a project before I start making changes to it.  I already compiled, so now, I program it into the board with either the Cypress Programmer or by running “mbed compile -f”.  When you attach a serial program to the development kit you will get something like this:

So, the project seems to work.  When I run the project again (by pressing the reset button on the board), here is what I get:

But what is it doing?  First, lets get the code into an editor where we can see what is happening:

Visual Studio Code

Recently, I have been using Visual Studio Code to view and edit my projects.  To make that experience better, it is a good idea to “export” the project from the MBED CLI.  This doesn’t change anything in your project, but it does create the files to make VSCODE work better.  To do this run “mbed export -i vscode_gcc_arm -m CY8CPROTO_062_4343W –profile mbed-os/tools/profiles/debug.json”

When you start VSCODE it will look something like this:

When I open the directory with my project with the “File -> Open …” menu

It will look like this:

Examine the Project

Now click on main.cpp and your screen should look like this:

To make any of the MBED OS Filesystems work, they need to have a “BlockDevice” to read and write the media, meaning the SPI Flash or SD Card or … The project as it comes from ARM creates the BlockDevice on line 23 where it asks for the “default_instance”.  Those configuration files which we patched MBED with earlier sets up the default instance to be the QSPI flash on the development kit (which I will explain in great detail in a later article).

After you have a BlockDevice, the next thing that you need is a FileSystem object.  In this case on line 31-33 you can see that this project uses a LittleFileSystem.  The argument to the LittleFileSystem object creation is the mount point (think Unix “/fs/”).  The mount point is used by all of the POSIX APIs (open, close, read etc).  I will talk more about POSIX in later article.

// This example uses LittleFileSystem as the default file system
#include "LittleFileSystem.h"
LittleFileSystem fs("fs");

Near the start of main, the first real thing that happens is that you need to “mount” the Filesystem onto the BlockDevice.  This is done on line 80.  The mount will return an non-zero error code if there is nothing on the SPI Flash or the SPI Flash is corrupted.  If the mount fails, the program will try to create a filesystem by calling “reformat” on line 87.  If that fails the “error” will halt the whole thing and blink the red light on the board.

    int err = fs.mount(bd);
    printf("%s\n", (err ? "Fail :(" : "OK"));
    if (err) {
        // Reformat if we can't mount the filesystem
        // this should only happen on the first boot
        printf("No filesystem found, formatting... ");
        fflush(stdout);
        err = fs.reformat(bd);
        printf("%s\n", (err ? "Fail :(" : "OK"));
        if (err) {
            error("error: %s (%d)\n", strerror(-err), err);
        }
    }

Once we have a Filesystem (object) and it is formatted, the project will try to open the file “/fs/numbers.txt” using the POSIX API “open” on line 97.  The open specifics that it is to open the file for “read” and that it will append the “+”.  If that operation fails, it will try to create the file on line 103.

 FILE *f = fopen("/fs/numbers.txt", "r+");
    printf("%s\n", (!f ? "Fail :(" : "OK"));
    if (!f) {
        // Create the numbers file if it doesn't exist
        printf("No file found, creating a new file... ");
        fflush(stdout);
        f = fopen("/fs/numbers.txt", "w+");
        printf("%s\n", (!f ? "Fail :(" : "OK"));
        if (!f) {
            error("error: %s (%d)\n", strerror(errno), -errno);
        }

If the file was opened for the first time, it will write the numbers 0-9 into the file using the loop (109) and fprintf (line 112).  The file will have lines with 4 spaces followed by a number then a “\n”.  This format was chosen to make the parsing easier later on in the program.

        for (int i = 0; i < 10; i++) {
            printf("\rWriting numbers (%d/%d)... ", i, 10);
            fflush(stdout);
            err = fprintf(f, "    %d\n", i);
            if (err < 0) {
                printf("Fail :(\n");
                error("error: %s (%d)\n", strerror(errno), -errno);
            }
        }
        printf("\rWriting numbers (%d/%d)... OK\n", 10, 10);

Once the file is initialized, you want the put the file point back to the start which is done with the “fseek” on line 122.

        printf("Seeking file... ");
        fflush(stdout);
        err = fseek(f, 0, SEEK_SET);
        printf("%s\n", (err < 0 ? "Fail :(" : "OK"));
        if (err < 0) {
            error("error: %s (%d)\n", strerror(errno), -errno);
        }

The main part of the program will start at the top,  read the numbers and increment them, and write them back into the file.  I am not really in love with this block of code… but I suppose that it is functional.

    // Go through and increment the numbers
    for (int i = 0; i < 10; i++) {
        printf("\rIncrementing numbers (%d/%d)... ", i, 10);
        fflush(stdout);

        // Get current stream position
        long pos = ftell(f);

        // Parse out the number and increment
        int32_t number;
        fscanf(f, "%d", &number);
        number += 1;

        // Seek to beginning of number
        fseek(f, pos, SEEK_SET);
    
        // Store number
        fprintf(f, "    %d\n", number);

        // Flush between write and read on same file
        fflush(f);
    }
    printf("\rIncrementing numbers (%d/%d)... OK\n", 10, 10);

Once all of the numbers are incremented and written back into the file, the last step is closing the file on line 156.

    // Close the file which also flushes any cached writes
    printf("Closing \"/fs/numbers.txt\"... ");
    fflush(stdout);
    err = fclose(f);
    printf("%s\n", (err < 0 ? "Fail :(" : "OK"));
    if (err < 0) {
        error("error: %s (%d)\n", strerror(errno), -errno);
    }

The next phase of the program is to do a directory listing using the POSIX directory APIs (opendir, readdir,closedir).  This little block of code will print out all of the files in the “/fs” directory.

  // Display the root directory
    printf("Opening the root directory... ");
    fflush(stdout);
    DIR *d = opendir("/fs/");
    printf("%s\n", (!d ? "Fail :(" : "OK"));
    if (!d) {
        error("error: %s (%d)\n", strerror(errno), -errno);
    }

    printf("root directory:\n");
    while (true) {
        struct dirent *e = readdir(d);
        if (!e) {
            break;
        }

        printf("    %s\n", e->d_name);
    }

    printf("Closing the root directory... ");
    fflush(stdout);
    err = closedir(d);
    printf("%s\n", (err < 0 ? "Fail :(" : "OK"));
    if (err < 0) {
        error("error: %s (%d)\n", strerror(errno), -errno);
    }

Then they demonstrate opening the numbers.txt file and printing out the data.

    // Display the numbers file
    printf("Opening \"/fs/numbers.txt\"... ");
    fflush(stdout);
    f = fopen("/fs/numbers.txt", "r");
    printf("%s\n", (!f ? "Fail :(" : "OK"));
    if (!f) {
        error("error: %s (%d)\n", strerror(errno), -errno);
    }

    printf("numbers:\n");
    while (!feof(f)) {
        int c = fgetc(f);
        printf("%c", c);
    }

    printf("\rClosing \"/fs/numbers.txt\"... ");
    fflush(stdout);
    err = fclose(f);
    printf("%s\n", (err < 0 ? "Fail :(" : "OK"));
    if (err < 0) {
        error("error: %s (%d)\n", strerror(errno), -errno);
    }

And finally closing things up by unmounting the filesystem.

   // Tidy up
    printf("Unmounting... ");
    fflush(stdout);
    err = fs.unmount();
    printf("%s\n", (err < 0 ? "Fail :(" : "OK"));
    if (err < 0) {
        error("error: %s (%d)\n", strerror(-err), err);
    }
        
    printf("Mbed OS filesystem example done!\n");

Super Annoying Hard Code

All through this example program the number “10” is hardcoded.  This is called a MAGIC NUMBER and in this particular case is not at all a good thing.  Moreover, lines of code like this represent absolute insanity.

    printf("\rIncrementing numbers (%d/%d)... OK\n", 10, 10);

Really… just don’t do this.  Friends don’t let friends use magic numbers.

Erasing the FileSystem

Near the top of main you can see that they register an interrupt to create an event when the button on the development kit is pressed.

   irq.fall(mbed_event_queue()->event(erase));

The erase function simply initializes the block device, calls erase and then de-inits the block device.  This will cause the whole thing to begin anew when the kit is reset.

void erase() {
    printf("Initializing the block device... ");
    fflush(stdout);
    int err = bd->init();
    printf("%s\n", (err ? "Fail :(" : "OK"));
    if (err) {
        error("error: %s (%d)\n", strerror(-err), err);
    }

    printf("Erasing the block device... ");
    fflush(stdout);
    err = bd->erase(0, bd->size());
    printf("%s\n", (err ? "Fail :(" : "OK"));
    if (err) {
        error("error: %s (%d)\n", strerror(-err), err);
    }

    printf("Deinitializing the block device... ");
    fflush(stdout);
    err = bd->deinit();
    printf("%s\n", (err ? "Fail :(" : "OK"));
    if (err) {
        error("error: %s (%d)\n", strerror(-err), err);
    }
}

The first time I ran the erase, I thought that there was something wrong… and I ended up going through a big debug loop.  The final step in the debug loop was being patient… which isn’t really in my wheelhouse.  I added this little block of code which timed the erase operation.

    Timer t;
    t.start();
    err = bd->erase(0,bd->size());
    t.stop();
    
    printf("%s\n", (err ? "Fail :(" : "OK"));
    if (err) {
        error("error: %s (%d)\n", strerror(-err), err);
    }
    printf("Time in s =%f\n",((double)t.read_ms())/1000.0);

And it turns out the answer is 115.06 seconds.  I am going to have to figure out why it takes so long.

The last thing to notice is that if you press the erase button while it is writing the files, Im pretty sure that something bad happens.

In the next articles I will examine this system in much much more detail.  Again thanks to Jaya and Vaira for their excellent work.

 

MBED OS & PSoC 6 & SSD1306

Summary

As I wrote about in the last article I have been working to get ready for Embedded World 2019 in a week and a bit.  For my demo, I will be handing out remote controls that have a 128×64 monochrome OLED display that is driven by an I2C SSD1306.  This whole board is controlled by a PSoC 6 & a 4343W WiFi / Bluetooth Combo.

This morning I started to port the U8G2 library to MBEDOS… but ran into some problems, so I i decided to see what ports were already out there.  I immediately found a port of the Adafruit_GFX library.  This article talks about using it on my CY8CPROTO_062_4343W board.  As part of this journey I also wanted to be able to draw the Cypress logo on the screen… so I had to figure out how to create a logo in a format that could be drawn on the screen.

I will follow these steps:

  1. Create a new project & add the Adafruit_GFX_library
  2. Create a main.cpp, configure the library and test
  3. Make a Cypress logo using GIMP
  4. Create a function to draw X11 bitmaps & test

Create a new project & add the Adafruit_GFX_library

The first step to get everything going by running

  1. mbed new .
  2. mbed add http://os.mbed.com/users/nkhorman/code/Adafruit_GFX/

The way to figure out how to add the library is by going to the library webpage on the mbedos website.  Then clicking the arrow on “Import into Compiler” where you will see two options, “Import into Compiler” and “Import with mbed CLI”

When you select that option you will get a window that tells you the exact command to run.

I have been using Atom as an editor (and sort of IDE).  When you open the lcd-example directory using Atom you will see your project including

  1. The mbed-os directory with all of the mbed stuff in it.
  2. The Adafruit_GFX library

Create a main.cpp, configure the library and test

The next step is to create a main.cpp.

  1. Setup the I2C.  In order to use the graphics library you need to setup a communication vehicle.  In my case that is an I2C bus that is connected to P6[0] and P6[1] on my development board.  Lines 6-15 create the communication class of I2CPreInit, configure it to 400kbs and connect the I2C master to P6[0]/P6[1]
  2. Line 16 actually setups up the graphics library and get it going.
  3. The main simply prints out some information about the display on lines 22-23
  4. Line 24 causes the current frame buffer to be displayed (more on this in a second)
  5. The main loop blinks the LED and prints a counter on the top of the screen.
#include "mbed.h"
#include "Adafruit_SSD1306.h"

DigitalOut myled(LED1);

class I2CPreInit : public I2C
{
public:
    I2CPreInit(PinName sda, PinName scl) : I2C(sda, scl)
    {
        frequency(400000);
        start();
    };
};
I2CPreInit gI2C(P6_1,P6_0);
Adafruit_SSD1306_I2c gOled2(gI2C,P0_2,0x78,64,128);

int main()
{   uint16_t x=0;

    printf("Started\n");
    printf("%ux%u OLED Display\r\n", gOled2.width(), gOled2.height());
    printf("Rotation = %u\n",gOled2.getRotation());
    gOled2.display();
    while(1)
    {
        x += 1;
        myled = !myled;
        gOled2.printf("%u\r",x);
        gOled2.display();
        wait(1.0);
    }
}

In order to build this thing I run “mbed compile -t GCC_ARM -m CY8CPROTO_062_4343w”.  When I run the project it looks like this:

There are several things to notice about this picture.  First, there is an Adafruit logo on the screen.  Where did this come from?  Simple on line 152 of Adafruit_ssd1306.cpp there is a function called “splash” which is called by the constructor.  The spash function just copies a bitmap into the frame buffer of the Adafruit_SSD1306 object.

void Adafruit_SSD1306::splash(void)
{
#ifndef NO_SPLASH_ADAFRUIT
	uint8_t adaFruitLogo[64 * 128 / 8] =
	{ 
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

The constructor is in Adafruit_ssd1306.h on line 152

	Adafruit_SSD1306_I2c(I2C &i2c, PinName RST, uint8_t i2cAddress = SSD_I2C_ADDRESS, uint8_t rawHeight = 32, uint8_t rawWidth = 128)
	    : Adafruit_SSD1306(RST, rawHeight, rawWidth)
	    , mi2c(i2c)
	    , mi2cAddress(i2cAddress)
	    {
		    begin();
		    splash();
		    display();
	    };

And if you don’t want to have this splash screen you can uncomment the #define NO_SPLASH_ADAFRUIT in the file “Adafruit_GFC_Config.h”

#ifndef _ADAFRUIT_GFX_CONFIG_H_
#define _ADAFRUIT_GFX_CONFIG_H_

// Uncomment this to turn off the builtin splash
#define NO_SPLASH_ADAFRUIT

// Uncomment this to enable all functionality
//#define GFX_WANT_ABSTRACTS

// Uncomment this to enable only runtime font scaling, without all the rest of the Abstracts
//#define GFX_SIZEABLE_TEXT


#endif

The next thing to notice in the picture is that I have lead wires attached to the LCD pins… and those wires are attached to a logic analyzer because I typed the I2C incorrectly and I couldn’t figure out why they didn’t talk.  And finally notice my grandfathers magnifying glass which I use every day.

Make a Cypress logo using GIMP

For my project I am less interested in displaying Adafruits Logo and more interested in displaying Cypress’.  To do this I loaded up the Cypress logo in Gimp.

I then converted it to pure black and white using the “Image->Mode->Indexed…”

Then selected “black and white palette”

Then I scaled the image to 128×40 using the “Image->Scale Image”

Unfortunately it made a bit of a mess of the logo during the scaling process… so I put my son to editing it.

Which looks like this after he was done.  Pretty good eh?

In order to use the image you need a “C program” version of it.  It turns out that there is a format called “X11” or “xbm” which is exactly that (a c-file).  You can read about the format on this website.  To get one of these files, just run “File->Export As”

Then give it a name with a “.xbm” on the end

Make sure and “de-select the X10 format bitmap” (and older version of the xbm format)

When all that is said and done you will find the xbm file with goodness in it.  Here is the top of it.

#define cylogo_width 128
#define cylogo_height 40
static unsigned char cylogo_bits[] = {
   0x00, 0x00, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x00, 0xe0, 0xff, 0x0f, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0xff, 0x3f,
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0xfe, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,

The format of this file is unsigned 8-bit integers… each bit represents the bit of one pixel… in BIG ENDIAN!!!! format.  In other words this table will be 128×40/8 bytes long.

Create a function to draw X11 bitmaps & test

But how do we use this format?  Well, write a new function in the Adafruit library to draw X11 bitmaps.

First add the new function name to the class on line 168 of “Adafruit_GFX.h”

    virtual void drawX11BitMap(const uint8_t bitmap[],uint16_t bitMapWidth,uint16_t bitMapSize,uint16_t posX,uint16_t posY);

Then add the code.

// Write an X11 formatted bitmap to the screen at posX, posY
void Adafruit_GFX::drawX11BitMap(const uint8_t bitmap[],uint16_t bitMapWidth,uint16_t bitMapSize,uint16_t posX,uint16_t posY)
{
  int16_t x1 = posX;
  int16_t y1 = posY;

  for(unsigned int i=0;i<bitMapSize;i++)
  {
    uint8_t val = bitmap[i];

    for(int j=0;j<8;j++)
    {
        uint16_t pixColor;
        if(val>>j & 0x01)
          pixColor = 1;
        else
          pixColor = 0;

        drawPixel(x1,y1, pixColor);
        x1 = x1 + 1;
        if(x1 == posX + bitMapWidth)
        {
          x1 = posX;
          y1 = y1 + 1;
        }
    }
  }

This may not be the most beautiful code in the world… which I suppose makes it fit right in with some of the other stuff in this driver.  Oh well it works.

Once you have added the function to the library, lets test it to see if it can draw the logo.  First, copy the “cylogo.xbm” into the project and call it “cylogo.h”.  Then modify the “main.cpp” to use it.  Add an include of the “cylogo.h”.  Then on line 26, call the function to draw it at 0,(half way down the screen)

#include "mbed.h"
#include "Adafruit_SSD1306.h"
#include "cylogo.h"
DigitalOut myled(LED1);

class I2CPreInit : public I2C
{
public:
    I2CPreInit(PinName sda, PinName scl) : I2C(sda, scl)
    {
        frequency(400000);
        start();
    };
};
I2CPreInit gI2C(P6_1,P6_0);
Adafruit_SSD1306_I2c gOled2(gI2C,P0_2,0x78,64,128);

int main()
{   uint16_t x=0;

    printf("Started\n");
    printf("%ux%u OLED Display\r\n", gOled2.width(), gOled2.height());
    printf("Rotation = %u\n",gOled2.getRotation());


    gOled2.drawX11BitMap(cylogo_bits,cylogo_width,sizeof(cylogo_bits),0,(64-cylogo_height)/2);

    gOled2.display();

When you program this… everything seems to be good.

By the way if it isn’t clear by now, I did a solder in a male header onto the board so that I could attach the I2C wires for the display.

MBEDOS & BLE & PSoC 6 & CYW4343W

Summary

At the Embedded World Show in Germany in a couple of weeks I am going to be showing a crazy demo (more on this later) that uses MBED OS and BLE and WiFi and PSoC 6 and the 4343W.  Given how close things are and how new MBED OS is to me I figure that I had better get going sorting out the BLE interface.   This article and probably the next several are going to show my progress through the learning curve.

It turns out that in MBED OS, instead of using the Cypress BLE Host Stack I will be using the ARM Cordio BLE host stack talking via HCI to the Cypress BLE Controller stack running on the 4343W (a Bluetooth, BLE and WiFi combo chip).  At this point all of my experience with BLE has been with Cypress stacks, either the PSoC 4/6 BLE stack or with the Cypress IoT stacks e.g. the CYW20719.  Lot’s of new learning.  Add in that all of the code is in C++ and it makes for an adventure.

For this article I will show the steps to get an ARM BLE example going on the CY8CPROTO_062_4343W development kit.  This will involve.

  1. Importing the ARM MBEDOS BLE Examples
  2. Modifying them to support the Cypress Targets & Test
  3. Updating an example program in a few places to fix things that I don’t like.

Import ARM MBED OS BLE Examples

The first step is to make a clone of the ARM examples by running “mbed import mbed-os-example-ble”.  This will load a bunch of different example projects (as libraries)

Then, when you look at what you have after all of that mess, you can see 14 example programs with promising names.

When you look in the BLE_LED directory you will find a file “readme.md” which is a markdown formatted file.  You can view this file on the GitHub website for this example here.  The top of this one looks promising:

Modify and Test

I decide that the example called “BLE_LED” looks like a good place to start.  This example is a simple peripheral that advertises it name.  When you connect to it there is a Service with UUID “0xA000” (unfortunately a 16-bit UUID… bad demo code) that Service has one characteristic with UUID 0xA001 (another 16-UUID … that isn’t nice … come on people… haven’t you read the spec?).  When you write a “1” to that characteristic the LED2 is supposed to turn on, and when you write a 0 the LED2 is supposed to turn off.

First, until the Cypress stuff is accepted into the main release, I need to update mbed-os to our targets) with “cd mbed-os ; mbed update master”.  To build this project Ill run “mbed compile -t GCC_ARM -m CY8CPROTO_062_4343W”.  When I program the the development kit, the LED starts blinking and I am able to see it using the GATT browser LightBlue Explorer.

But when I try to write a 1 to the 0xA001 characteristic nothing happens.

So, what gives? The answer is that on line 32 you can see that the authors as assuming that you have two LEDs (my development kit only has one.

        _alive_led(LED2, 1),
        _actuated_led(LED1, 0),

And on line 124 you can see a function that inverts the LED

void blink() {
        _alive_led = !_alive_led;
    }

which is triggered on line 47 to be called every 500ms

    void start() {
        _ble.gap().setEventHandler(this);

        _ble.init(this, &LEDDemo::on_init_complete);

        _event_queue.call_every(2000, this, &LEDDemo::blink);

        _event_queue.dispatch_forever();
    }

OK.  I am not loving this. I think that I should make some updates to this project.

Update

There are several things that I don’t like about this program or need to be fixed.

  1. Make the user LED2 be LED1 and fix the fact that it is active low.
  2. Change the UUIDs of the Service and Characteristic to be legal 128-bit UUIDs
  3. Make the stdio print out the status of the connection (instead of the blinking LED1)
  4. Make the baud rate of standard i/o be 115200 instead of 9600

First, fix the LED2 to be LED1.  Do this by commenting out all of the _alive_led code and switching the _actuated_led to be LED1.  Also set the state of the LED1 to 1 (meaning off because it is active low)

        //_alive_led(LED1, 1),
        _actuated_led(LED1, 1),

The author of the example code has a function called blink which is executed by the event queue every 500ms, comment out that function

/*
    void blink() {
        _alive_led = !_alive_led;
    }
*/

And don’t inject events into the queue to run the blink function

        //_event_queue.call_every(500, this, &LEDDemo::blink);

The LED on my board is active low… so instead of writing the value write the opposite of the value.

            _actuated_led = !*(params->data);

It is illegal in Bluetooth to use 16-bit UUIDs without first registering them with the Bluetooth SIG and having them be “Assigned numbers”.  The author of this example program violated the specification by assigning the LED service UUID of 0xA000 and the LED characteristic UUID of 0xA001.  This is super annoying and I am not willing to be a slob.  To fix this modify ledservice.h to declare the UUIDs as UUID type instead of uint16_ts

    //const static uint16_t LED_SERVICE_UUID              = 0xA000;
    //const static uint16_t LED_STATE_CHARACTERISTIC_UUID = 0xA001;

    const static UUID LED_SERVICE_UUID;
    const static UUID LED_STATE_CHARACTERISTIC_UUID;

Then initialize them in the main.cpp as 128-bit UUIDs using the const char * initializer.

const UUID LEDService::LED_SERVICE_UUID("21c04d09-c884-4af1-96a9-52e4e4ba195b");
const UUID LEDService::LED_STATE_CHARACTERISTIC_UUID("1e500043-6b31-4a3d-b91e-025f92ca9763");

The original code has a blinking LED.  Which I dont really like.  Typically, I like to blink the LED when the device is advertising, and make it be solid when there is a connection.  However, as I only have one LED on my board, and I have allocated it to be the “_actuated_led”, I will use the UART to print out status changes.  To do this, I update the “onDisconnectionComplete” and “onConnectionComplete” events to print out that fact to stdio.

    void onDisconnectionComplete(const ble::DisconnectionCompleteEvent&) {
        _ble.gap().startAdvertising(ble::LEGACY_ADVERTISING_HANDLE);
        printf("DisconnectionCompleteEvent\n");
    }

    void onConnectionComplete	(	const ble::ConnectionCompleteEvent & 	event	)
    {
      printf("onConnectionComplete\n");
    }

In order to set the stdio to use 115200 instead of 9600 you can change the default rate of the UART in the mbed_app.json.

  "CY8CPROTO_062_4343W": {
            "platform.stdio-baud-rate": 115200,
            "platform.default-serial-baud-rate": 115200
        },

Here is the final version of main.cpp

/* mbed Microcontroller Library
 * Copyright (c) 2006-2013 ARM Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <events/mbed_events.h>
#include <mbed.h>
#include "ble/BLE.h"
#include "LEDService.h"
#include "pretty_printer.h"

const static char DEVICE_NAME[] = "LED";

static EventQueue event_queue(/* event count */ 10 * EVENTS_EVENT_SIZE);

//const UUID::LongUUIDBytes_t testbytes = { 0x21, 0xc0, 0x4d, 0x09, 0xc8, 0x84, 0x4a, 0xf1, 0x96, 0xa9, 0x52, 0xe4, 0xe4, 0xba, 0x19, 0x5b } ;
// {0x1e, 0x50, 0x00, 0x43, 0x6b, 0x31, 0x4a, 0x3d, 0xb9, 0x1e, 0x02, 0x5f, 0x92, 0xca, 0x97, 0x63}
//const UUID LEDService::LED_SERVICE_UUID(testbytes,UUID::MSB);
const UUID LEDService::LED_SERVICE_UUID("21c04d09-c884-4af1-96a9-52e4e4ba195b");
const UUID LEDService::LED_STATE_CHARACTERISTIC_UUID("1e500043-6b31-4a3d-b91e-025f92ca9763");

class LEDDemo : ble::Gap::EventHandler {
public:


    LEDDemo(BLE &ble, events::EventQueue &event_queue) :
        _ble(ble),
        _event_queue(event_queue),
        //_alive_led(LED1, 1),
        _actuated_led(LED1, 1),
        _led_uuid(LEDService::LED_SERVICE_UUID),
        _led_service(NULL),
        _adv_data_builder(_adv_buffer) { }

    ~LEDDemo() {
        delete _led_service;
    }

    void start() {
        _ble.gap().setEventHandler(this);

        _ble.init(this, &LEDDemo::on_init_complete);

        //_event_queue.call_every(500, this, &LEDDemo::blink);

        _event_queue.dispatch_forever();
    }

private:
    /** Callback triggered when the ble initialization process has finished */
    void on_init_complete(BLE::InitializationCompleteCallbackContext *params) {
        if (params->error != BLE_ERROR_NONE) {
            printf("Ble initialization failed.");
            return;
        }

        _led_service = new LEDService(_ble, false);

        _ble.gattServer().onDataWritten(this, &LEDDemo::on_data_written);

        print_mac_address();

        start_advertising();
    }

    void start_advertising() {
        /* Create advertising parameters and payload */

        ble::AdvertisingParameters adv_parameters(
            ble::advertising_type_t::CONNECTABLE_UNDIRECTED,
            ble::adv_interval_t(ble::millisecond_t(1000))
        );

        _adv_data_builder.setFlags();
        _adv_data_builder.setLocalServiceList(mbed::make_Span(&_led_uuid, 1));
        _adv_data_builder.setName(DEVICE_NAME);

        /* Setup advertising */

        ble_error_t error = _ble.gap().setAdvertisingParameters(
            ble::LEGACY_ADVERTISING_HANDLE,
            adv_parameters
        );

        if (error) {
            printf("_ble.gap().setAdvertisingParameters() failed\r\n");
            return;
        }

        error = _ble.gap().setAdvertisingPayload(
            ble::LEGACY_ADVERTISING_HANDLE,
            _adv_data_builder.getAdvertisingData()
        );

        if (error) {
            printf("_ble.gap().setAdvertisingPayload() failed\r\n");
            return;
        }

        /* Start advertising */

        error = _ble.gap().startAdvertising(ble::LEGACY_ADVERTISING_HANDLE);

        if (error) {
            printf("_ble.gap().startAdvertising() failed\r\n");
            return;
        }
    }

    /**
     * This callback allows the LEDService to receive updates to the ledState Characteristic.
     *
     * @param[in] params Information about the characterisitc being updated.
     */
    void on_data_written(const GattWriteCallbackParams *params) {
        if ((params->handle == _led_service->getValueHandle()) && (params->len == 1)) {
            _actuated_led = !*(params->data);
        }
    }
/*
    void blink() {
        _alive_led = !_alive_led;
    }
*/
private:
    /* Event handler */

    void onDisconnectionComplete(const ble::DisconnectionCompleteEvent&) {
        _ble.gap().startAdvertising(ble::LEGACY_ADVERTISING_HANDLE);
        printf("DisconnectionCompleteEvent\n");
    }

    void onConnectionComplete	(	const ble::ConnectionCompleteEvent & 	event	)
    {
      printf("onConnectionComplete\n");
    }

private:
    BLE &_ble;
    events::EventQueue &_event_queue;
    //DigitalOut _alive_led;
    DigitalOut _actuated_led;

    UUID _led_uuid;
    LEDService *_led_service;

    uint8_t _adv_buffer[ble::LEGACY_ADVERTISING_MAX_SIZE];
    ble::AdvertisingDataBuilder _adv_data_builder;
};

/** Schedule processing of events from the BLE middleware in the event queue. */
void schedule_ble_events(BLE::OnEventsToProcessCallbackContext *context) {
    event_queue.call(Callback<void()>(&context->ble, &BLE::processEvents));
}

int main()
{
    printf("Example Bluetooth\n");
    BLE &ble = BLE::Instance();
    ble.onEventsToProcess(schedule_ble_events);

    LEDDemo demo(ble, event_queue);
    demo.start();

    return 0;
}

And LEDService.h

/* mbed Microcontroller Library
 * Copyright (c) 2006-2013 ARM Limited
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifndef __BLE_LED_SERVICE_H__
#define __BLE_LED_SERVICE_H__

class LEDService {
public:
    //const static uint16_t LED_SERVICE_UUID              = 0xA000;
    //const static uint16_t LED_STATE_CHARACTERISTIC_UUID = 0xA001;

    const static UUID LED_SERVICE_UUID;
    const static UUID LED_STATE_CHARACTERISTIC_UUID;

    LEDService(BLEDevice &_ble, bool initialValueForLEDCharacteristic) :
        ble(_ble), ledState(LED_STATE_CHARACTERISTIC_UUID, &initialValueForLEDCharacteristic)
    {
        GattCharacteristic *charTable[] = {&ledState};
        GattService         ledService(LED_SERVICE_UUID, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));

        ble.gattServer().addService(ledService);
    }

    GattAttribute::Handle_t getValueHandle() const
    {
        return ledState.getValueHandle();
    }

private:
    BLEDevice                         &ble;
    ReadWriteGattCharacteristic<bool> ledState;
};

#endif /* #ifndef __BLE_LED_SERVICE_H__ */

 

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.