Cypress Type-C Barrel Connector Replacement + Infineon Buck DC/DC (Part 2)

Summary

In this article I will show you how to use a Keithley 2380 (actually two different ones) to test the output of the IRDC3894 12V->1.2V 12A buck development kit.

The Story

To this point I have written several articles about my process of designing a power supply for my new IoT device.  It needs to provide for quite a bit of power, actually 60W is what I am planning on.  I really wanted to make sure that the IR3894 chip would do what it says it would, specifically supply 12A.  The development kit is pretty simple.  There are two banana plug to  provide power to Vin and two banana plus for the load.

For this round of tests I will Keithley 2230-30-1 to provide power and I will use my Keithley 2380-120-60 to serve as the load.

The two mini grabbers are attached to to remote sensing terminals on the Keithley 2380.

After I had it all hooked up I went in 1A increments from 0 to 12A, then I went in 0.1A increments until I ran out of input power.

Here is the actual data table.  Note that I added columns to show the calculated input power.  And I calculated the efficiency of the system Wout/Win

Vin Iin Win Vout Iout W Eff
12 0.27 3.24 1.198 0 0 0%
12 0.129 1.548 1.198 0.998 1.195604 77%
12 0.239 2.868 1.198 1.998 2.393604 83%
12 0.345 4.14 1.198 2.998 3.591604 87%
12 0.454 5.448 1.198 3.998 4.789604 88%
12 0.564 6.768 1.198 4.999 5.988802 88%
12 0.677 8.124 1.198 5.998 7.185604 88%
12 0.792 9.504 1.198 6.998 8.383604 88%
12 0.909 10.908 1.198 7.998 9.581604 88%
12 1.029 12.348 1.198 8.998 10.779604 87%
12 1.152 13.824 1.198 9.999 11.978802 87%
12 1.277 15.324 1.198 10.998 13.175604 86%
12 1.406 16.872 1.198 11.997 14.372406 85%
12 1.42 17.04 1.198 12.098 14.493404 85%
12 1.434 17.208 1.198 12.198 14.613204 85%
12 1.448 17.376 1.198 12.297 14.731806 85%
12 1.462 17.544 1.198 12.398 14.852804 85%
12 1.477 17.724 1.198 12.498 14.972604 84%
12 1.49 17.88 1.198 12.59 15.08282 84%

When I plot the data there is something sticking out like a sore thumb.  WTF?  At first I assume that I typed in the wrong number when I transposed the hand written data to the spreadsheet.  So I went and looked at the data table where it appears that I typed it in correctly.  Does the efficiency really have a peak like that?

I decided to go remeasure the 5A datapoint.

Then I looked at my handwritten data sheet where I find that I transposed the last two digits of the input current. (I definitely should automate this measurement)

OK… now the plot looks way better

When I compare the plot from the data sheet versus my data on the same scale (about) they look very similar.  All seems good.

 

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”

PyVISA First Use

Summary

In this article I will show you how to install PyVISA, and use it to control to a Keithley 2230-30-1 Triple Channel DC Power Supply.

The Story

I have been working on a power supply for a new IoT device that I am building you can read about it here.  This power supply uses an Infineon IR3894 DC buck converter to turn 20V from a Type-C wall wart into a usable 5V for my system.  In the IRDC3894 data sheet there is a graph of efficiency versus load current that I would like to duplicate with real measurements.

In order to make this graph you need to:

  1. Turn on the Keithley 2280-30-1 power supply (that is simulating the Type-C power supply)
  2. Set the Keithley 2380-500-15 load to +1.2A (starting at 0)
  3. Read the input voltage and current
  4. Read the output voltage and current
  5. Loop back to (2) until you get to 12A
  6. Calculate the input and output power at each of the point
  7. Plot the data

Obviously this could all be done by hand… but I know that my Power supply and DC load are both programmable over “GPIB”.  I also know that theoretically I could get National Instruments Labview and it would probably know how to do something like that.  BUT… what I really wanted was to write code in Python.

So I posted on forum.tek.com and their excellent Apps engineer “Andrea C.” replied:

I have another (future) article where I will write extensively about the bewildering jungle of Test Automation.  But for today Ill say that VISA stands for Virtual Instrument Software Architecture which is basically a standard way to talk to Lab Instruments.  And, NI-VISA is the National Instrument implementation of VISA.  But wait, a quick google search lead me to the Open Source project PyVISA.  So, I’ll start there.

There will also be a future in-depth discussion of PyVisa, but this is the first step.

PyVISA

For this example, I’ll show you the simple steps of turning on the power supply and printing out the current, voltage and calculated power as retrieved from the power supply.

I always like to use a virtual environment for working with Python on my MacBook (or any computer for that matter).  So, I start by creating the virtual environment.  Then I run the PIP installer to install PyVisa (and all of its requirements).

The specific commands are:

Command Comment
python3 -m venv venv Create a Python3 virtual environment
source venv/bin/activate Turn on the virtual environment
pip install pyusb Install the Python usb interface (so that PyVISA can search for devices on the USB bus)
pip install pyvisa Install the PyVisa environment
pip install pyvisa-py Install the low level python drivers for PyVisa to use to talk to the bus

Then I tested the installation by running

Command Comment
python Startup an interactive Python environment
import visa Load the PyVISA module
rm = visa.ResourceManager() Attach to the interface
rm.list_resources() Get a list of all VISA devices attached to the interface

Here is what the screen looks like:

SCPI & Keithley 2230-30-1

OK, we have a working Python environment that can talk to the Power Supply.  Now what? In the unbelievable chaos of talking to Test Instruments, the vendors got together and agreed to a “Standard Commands for Programmable Instruments” also called SCPI.  In order to learn about SCPI (in the context of this power supply) you can read Chapter 4 of the Keithley Power Supply user manual.  This chapter describes how commands work etc.

After the general section there is an entire chapter (5) that discusses the specific commands for this instrument.

And if you look down into the document you will find a description of each of the commands.  For instance if you issue the command “SYS:REM” it will let you take control of the instrument.

Run the Test

Now that we know everything is working, I write a Python program to do the needful.

When you start the program the power supply is off and the load is set to 2A (but has no power being applied)

When you run the program it turns on the power supply and grabs control of device.  The little symbol next to the 12 says that the device is being control via USB.

Finally here is the source code:

import visa
import time
rm = visa.ResourceManager()
vm = rm.open_resource('USB0::1510::8752::9202095::0::INSTR')

# Turn on remote mode (so that SCPI commands work)
vm.write('SYST:REM')
vm.write('APPL CH1, 12V, 1A')
vm.write('OUTP ON')
print('Power on... waiting for settling')
# need a little delay right here
time.sleep(3)

print('Taking measurements')
voltCH1 = float(vm.query('MEAS:VOLT? CH1'))
currentCH1 = float(vm.query('MEAS:CURR? CH1'))
powerCH1 = voltCH1 * currentCH1

print(f'V={voltCH1:.1f}V I={currentCH1:.2f}A P={powerCH1:.2f}W')

vm.write('OUTP OFF')
print('Turned off power')

 

Keithley 2380-500-15 Programmable Load Remote Sensing

Summary

In this article I will show you how to install and use the remote sensing inputs for the Keithley 238-500-15 Programmable Load.

The Problem

In a previous article I talked about a power supply that I was working on.  In that design, I will be taking 20V@3A from a USB Type-C power supply and turning it into 5V@12A for use to drive a PSoC and a bunch of LEDs.  For testing the setup, I use a Keithley 2380-500-15 Programmable Load that I got from Mouser.  The device is really cool as it can be programmed to provide constant current, constant resistance, constant power or constant voltage.  For this test, I am using the 2380-500-15 as constant current load to pull current out of the Infineon IRDC3894 development board which produces 1.2v@12A.

In my first setup I got this result:  The board is generating 1.2V (which you can see on the top DMM).  And the 2380-500-15 is showing 1.204V.  The same, thats good.

But, when I turn on the current to 1A you can see that the 2380-500-15 is showing 1.15V.  What happened?  Is the power supply drooping?

And it gets worse at 4A

So, I posted on the Tek Forum, and the excellent Tek engineer Andrea C. gave me an answer and posted a nice picture.  The problem is that my test wire to the 2380-500-15 has resistance and the voltage is dropping through the test wire.  You mean test-wires have resistance 🙂  Here is the picture that she posted.

The Solution

So, what is the answer to this problem?  Remote sensing which will display the voltage at the source instead of at the end of the wire.  In the world this method is more commonly called “4 terminal sensing” or “Kelvin Sensing”

The 2380 is capable of using this method.  And it is described (poorly) on page 52 of the user guide.  Here is the picture:

To make this work you need to do two things

  1. Attach two wires on the back panel sensing connection
  2. Switch the load into remote sensing mode.

Back Panel

When you look at the back panel you see this crazy terminal block.  The first thing you notice is that some of the screw terminals are blocked by the BNC connector.  And the angle for putting a screw driver isn’t great.

But, if you loosen the two flat head screws on the far left and right of the green connector, you will be able to pull it out.  It has a little bit of “snap” so you need to pull on it a bit (just with your fingers so you don’t break it).  You will then be able to see into the connector.

Here is what the green connector looks like once it is removed.

Notice that each of the terminals have a screw to tighten the corresponding wire.

In order to hook to the screw terminals you need a cable.  I bought this one from Mouser

Then I clipped off the Banana plug, and tinned the ends of the wire with solder (you can see the black wire is a bit flattened because I took this picture after I unhooked the wire)

Then screwed the wires into the connector

Finally screwed the connector back into the meter.

Enable Remote Sensing

To enable remote sensing, the documentation tells you what to do:

Here are pictures of the screens.  When you press Shift-9 you get this screen.  You need to press the “right arrow” to go to the next screen.

Select the “Remote-Sense” and press enter.

Use the left/right arrow to select “On” and press the “Enter”

Results

Now it displays “1.204V” which matches the DMM (well pretty close)

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.

PSoC 6, DMA & WS2812 LEDs – Modus Toolbox

Summary

One of my favorite readers, who also happens to be my bosses, bosses boss sent me an email the other day asking about the WS2812 LEDs.  So, I sent him a link to my previous article about PSOC 6 and DMA and WS2812.  He said, “That’s cool and everything… but do you have it in Modus Toolbox”.  Well, you wish is my command.

In the original article I wrote directly on the bare metal.  Which is something that I don’t really like, so in this article I will port the original code to use FreeRTOS.  In addition, in the original article I used a CY8CPROTO-062-4343W.  But, look what I found in the mail the other day.  YES! Ronak sent me a prototype of the new Cypress development kit.  Sweet.  Here is a picture.  It has a P6 and a CYW43012 (low power Bluetooth and WiFi).

For this article I will follow these steps:

  1. Make a new project
  2. Add middleware
  3. Configure the retarget i/o, the red LED & Test the configuration
  4. Explain the 2812 Task Architecture
  5. Create ws2812.h
  6. Create ws2812.c
  7. Update main.c to use the public interface of ws2812.h
  8. Rewire to use a level shifter

Finally, I will discuss some other ideas that I have for the project.

Make a New Project

In the quick panel select “New Application”.

Pick out the “CY8CKIT-062-4343W” which has the same PSoC.  In fact any of the CY8C624ABZI-D44 kits will work.

Use the “EmptyPSoC6App” starter project and give it the name “ws2812-mtb”

Select “Finish”

Add the Middleware

For this project I want to use several pieces of middleware.  To add them, right click on the project and select “ModusToolbox Middleware Selector”

Pick out FreeRTOS, Capsense, and Retarget I/O

Press OK, which will bring all of the right libraries into your project.

Configure the retarget i/o, the red LED & Test the configuration

Before I get too far down the road I like to test and make sure that the basic stuff is working.  So, I start by configuring the hardware I need for Retarget I/O and the blinking LED.  To do the hardware configuration, select “Configure Device” from the quick panel.

On this board the Red LED is connected to P1[1].  Here is a picture of the very nice label on the back. (notice the engineering sample sticker)

Go to the pins tab, turn on P1[1], give it the name “red” and select the strong drive mode.

To use the Retarget I/O you need a UART.  Go to the Peripheral tab and turn on “Serial Communication Block (SCB) 5”  Tell it to use P5[0] and P5[1] and the 0th 8-bit clock divider.  Then press save.

Open up studio_user.h and setup the standard i/o to use the correct SCB which we made an alias to called UART_STDIO_HW.  You need to add the include “cycfg.h” so that it can find the alias configuration file.

#include "cy_device_headers.h"
#include "cycfg.h"
/* Must remain uncommented to use this utility */
#define IO_STDOUT_ENABLE
#define IO_STDIN_ENABLE
#define IO_STDOUT_UART      UART_STDIO_HW
#define IO_STDIN_UART       UART_STDIO_HW

and then edit main.c.

  1. Add the include for stdio.h (line 31)
  2. Add the include for FreeRTOS.h (line 32)
  3. Add the include for the task.h (line 33)
  4. Make a context for the UART SCB (line 35)
  5. Write the function for the blinking LED task (line 37-45)
  6. Initialize the SCB as a UART and enable it (lines 53-54)
  7. Print a test message (line 58)
  8. Create the task (line 60)
  9. Start the scheduler (line 61)
#include "cy_device_headers.h"
#include "cycfg.h"
#include <stdio.h>
#include "FreeRTOS.h"
#include "task.h"
cy_stc_scb_uart_context_t UART_STDIO_context;
void ledTask(void *arg)
{
(void)arg;
while(1)
{
Cy_GPIO_Inv(red_PORT,red_PIN);
vTaskDelay(1000);
}
}
int main(void)
{
/* Set up the device based on configurator selections */
init_cycfg_all();
Cy_SCB_UART_Init(UART_STDIO_HW,&UART_STDIO_config,&UART_STDIO_context);
Cy_SCB_UART_Enable(UART_STDIO_HW);
__enable_irq();
printf("Hello world\n");
xTaskCreate(ledTask,"LED Task",100,0,5,0);
vTaskStartScheduler();
}

Once you program it you should have a blinking LED + a serial terminal that says “Hello world”

Now that you having a working test jig we will turn ourselves to fixing up the ws2812 driver.

Configure the SPI and DMA

As I discussed in the previous article the I use the SPI to drive the sequence of 110 (for 1’s) or 100 (for 0’s) out to the string of WS2812B LEDs.  The only difference is that this time I will use  SCB0 and P0[2].  Why?  I wanted to save all of the pins on the Arduino  headers for the display.  This lead me to the row of pins on the outside of the header labeled IO0->IO7

Then I looked at the schematic and found:

OK I know what the pins are, but how do I know which SCB to attach to?  I started up the device configurator, then went through each of the pins, enabled them, then looked at what the digital inout was attached to by clicking on the dropdown menu.   In the picture below you can see that P0[2] is connected to SCB0 SPI.mosi.

Now I know SCB0. You can read about how I chose the SPI configurations values in the previous article, but for today choose:

  • SCB=SCB0
  • master
  • cpha=1 cpol=1
  • oversample=4
  • clk = clk1
  • MOSI = P0[2]
  • Tx trigger = DMA0 Channel 16

The next step is to turn on the DMA block DMA Datawire 0: Channel 16.  I am going to copy the configuration files from the PSoC Creator project, so all I need is the alias for the block

WS2812 Task Architecture

In the original article I have one flat main.c file (actually main_cm4.c)  But, when I look back, I should have used an RTOS (bad Alan).  Basically, I am going to copy the original main_cm4.c and hack it up into a new architecture.  My program will have a task called ws2812Task which will manage the LEDs.  The task will “sit” on a queue that is waiting for the rest of the system to send command messages.  Those messages are in the following format:

typedef enum {
ws2812_cmd_update,            /* no arguments */
ws2812_cmd_autoUpdate,        /* data is a binary true for autoupdate false for no update  */
ws2812_cmd_setRGB,            /* data is pixel number + rgb                                */
ws2812_cmd_setRange,          /* data is 0xFF00 bits for start and 0x00FF bits for y + rgb */
ws2812_cmd_initMixColorRGB,   /* no arguments, turns led string to rgbrgbrgb...                */
}ws2812_cmd_t;
typedef struct {
ws2812_cmd_t cmd;
uint32_t data;
uint8_t red;
uint8_t green;
uint8_t blue;
} ws2812_msg_t;

In addition I will create some public functions which will setup a message and submit it into the queue.  The last piece of the puzzle is that I will have a software timer which will run every 30ms to update the LEDs (if the timer is running)

Create ws2812.h

The public interface to my ws2812Task will reside in a new file called “ws2812.h”.  It is pretty simple

  • Define the number of LEDs
  • Define the enumerated list of legal commands
  • Define the Queue structure ws2812_msg_t (lines
  • 5 helper functions which create a command message and submit it into the queue (lines 15-19)
  • the function prototype for the ws2812Task (line 19)
/*
* ws2812.h
*
*  Created on: Jun 15, 2019
*      Author: arh
*/
#ifndef WS2812_H_
#define WS2812_H_
#include "stdbool.h"
#include "FreeRTOS.h"
#include "queue.h"
#define ws2812_NUM_PIXELS (144)
extern QueueHandle_t ws2812QueueHandle;
typedef enum {
ws2812_cmd_update,            /* no arguments */
ws2812_cmd_autoUpdate,        /* data is a binary true for autoupdate false for no update  */
ws2812_cmd_setRGB,            /* data is pixel number + rgb                                */
ws2812_cmd_setRange,          /* data is 0xFF00 bits for start and 0x00FF bits for y + rgb */
ws2812_cmd_initMixColorRGB,   /* no arguments, turns string to rgbrgbrgb...                */
}ws2812_cmd_t;
typedef struct {
ws2812_cmd_t cmd;
uint32_t data;
uint8_t red;
uint8_t green;
uint8_t blue;
} ws2812_msg_t;
extern QueueHandle_t ws2812QueueHandle;
void ws2812_update(void);
void ws2812_autoUpdate(bool option);
void ws2812_setRGB(int led,uint8_t red, uint8_t green, uint8_t blue);
void ws2812_setRange(int start, int end, uint8_t red,uint8_t green ,uint8_t blue);
void ws2812_initMixColorRGB(void);
void ws2812Task(void *arg);
#endif /* WS2812_H_ */

Create ws2812.c

To build the ws2812.c I start by opening the main_cm4.c from the original project and copying it into the ws2812.c.  At the top I add the includes for ws2812.h and the includes for FreeRTOS.  Next I declare the handle for the Queue and the Timer.  I wanted to have a variable which kept track of the autoUpdate timer being turned on, so I declare a bool.  The rest of the code is from the original program.

#include "ws2812.h"
#include "FreeRTOS.h"
#include "queue.h"
#include "timers.h"
QueueHandle_t ws2812QueueHandle;
TimerHandle_t ws2812TimerHandle;
bool wsAutoUpdateState = false;
#define WS_ZOFFSET (1)
#define WS_ONE3  (0b110<<24)
#define WS_ZERO3 (0b100<<24)
#define WS_SPI_BIT_PER_BIT (3)
#define WS_COLOR_PER_PIXEL (3)
#define WS_BYTES_PER_PIXEL (WS_SPI_BIT_PER_BIT * WS_COLOR_PER_PIXEL)
static uint8_t WS_frameBuffer[ws2812_NUM_PIXELS*WS_BYTES_PER_PIXEL+WS_ZOFFSET];

Next I build the 5 helper functions.  These functions all have exactly the same form,

  • declare a ws2812_msg_t
  • fill it up
  • send it to the queue

Notice that I wait 0 time to try to add to the queue.  What that means is if the queue is full the message will get tossed away.

// These functions are helpers to create the message to send to the ws2812 task.
void ws2812_update(void)
{
ws2812_msg_t msg;
msg.cmd = ws2812_cmd_update;
xQueueSend(ws2812QueueHandle,&msg,0);
}
void ws2812_autoUpdate(bool option)
{
ws2812_msg_t msg;
msg.cmd = ws2812_cmd_autoUpdate;
msg.data = option;
xQueueSend(ws2812QueueHandle,&msg,0);
}
void ws2812_setRGB(int led,uint8_t red, uint8_t green, uint8_t blue)
{
ws2812_msg_t msg;
msg.cmd = ws2812_cmd_setRGB;
msg.red = red;
msg.blue = blue;
msg.green = green;
msg.data = led;
xQueueSend(ws2812QueueHandle,&msg,0);
}
void ws2812_setRange(int start, int end, uint8_t red,uint8_t green ,uint8_t blue)
{
ws2812_msg_t msg;
msg.cmd = ws2812_cmd_setRange;
msg.red = red;
msg.blue = blue;
msg.green = green;
msg.data = start << 16 | end;
xQueueSend(ws2812QueueHandle,&msg,0);
}
void ws2812_initMixColorRGB(void)
{
ws2812_msg_t msg;
msg.cmd = ws2812_cmd_initMixColorRGB;
xQueueSend(ws2812QueueHandle,&msg,0);
}

The next block of code is largely unchanged from the original program, except where I fixed some small differences between the PSoC Creator generated code and the ModusToolbox generated code.

// Function WS_DMAConfiguration
// This function sets up the DMA and the descriptors
#define WS_NUM_DESCRIPTORS (sizeof(WS_frameBuffer) / 256 + 1)
static cy_stc_dma_descriptor_t WSDescriptors[WS_NUM_DESCRIPTORS];
static void WS_DMAConfigure(void)
{
// I copies this structure from the PSoC Creator Component configuration
// in generated source
const cy_stc_dma_descriptor_config_t WS_DMA_Descriptors_config =
{
.retrigger       = CY_DMA_RETRIG_IM,
.interruptType   = CY_DMA_DESCR_CHAIN,
.triggerOutType  = CY_DMA_1ELEMENT,
.channelState    = CY_DMA_CHANNEL_ENABLED,
.triggerInType   = CY_DMA_1ELEMENT,
.dataSize        = CY_DMA_BYTE,
.srcTransferSize = CY_DMA_TRANSFER_SIZE_DATA,
.dstTransferSize = CY_DMA_TRANSFER_SIZE_WORD,
.descriptorType  = CY_DMA_1D_TRANSFER,
.srcAddress      = NULL,
.dstAddress      = NULL,
.srcXincrement   = 1L,
.dstXincrement   = 0L,
.xCount          = 256UL,
.srcYincrement   = 0L,
.dstYincrement   = 0L,
.yCount          = 1UL,
.nextDescriptor  = 0
};
for(unsigned int i=0;i<WS_NUM_DESCRIPTORS;i++)
{
Cy_DMA_Descriptor_Init(&WSDescriptors[i], &WS_DMA_Descriptors_config);
Cy_DMA_Descriptor_SetSrcAddress(&WSDescriptors[i], (uint8_t *)&WS_frameBuffer[i*256]);
Cy_DMA_Descriptor_SetDstAddress(&WSDescriptors[i], (void *)&WS_SPI_HW->TX_FIFO_WR);
Cy_DMA_Descriptor_SetXloopDataCount(&WSDescriptors[i],256); // the last
Cy_DMA_Descriptor_SetNextDescriptor(&WSDescriptors[i],&WSDescriptors[i+1]);
}
// The last one needs a bit of change
Cy_DMA_Descriptor_SetXloopDataCount(&WSDescriptors[WS_NUM_DESCRIPTORS-1],sizeof(WS_frameBuffer)-256*(WS_NUM_DESCRIPTORS-1)); // the last
Cy_DMA_Descriptor_SetNextDescriptor(&WSDescriptors[WS_NUM_DESCRIPTORS-1],0);
Cy_DMA_Descriptor_SetChannelState(&WSDescriptors[WS_NUM_DESCRIPTORS-1],CY_DMA_CHANNEL_DISABLED);
Cy_DMA_Enable(WS_DMA_HW);
}
// Function: WS_DMATrigger
// This function sets up the channel... then enables it to dump the frameBuffer to pixels
void WS_DMATrigger()
{
cy_stc_dma_channel_config_t channelConfig;
channelConfig.descriptor  = &WSDescriptors[0];
channelConfig.preemptable = false;
channelConfig.priority    = 3;
channelConfig.enable      = false;
Cy_DMA_Channel_Init(WS_DMA_HW, WS_DMA_CHANNEL, &channelConfig);
Cy_DMA_Channel_Enable(WS_DMA_HW,WS_DMA_CHANNEL);
}

The next block of code is just a function which the autoupdate timer can call to trigger the DMA to update the stripe of LEDs.

// This function is called by the software timer which is used to autoupdate the LEDs
// It checks to make sure that the DMA is done... if not it doesnt do anything
void ws2812CallbackFunction( TimerHandle_t xTimer )
{
if((Cy_DMA_Channel_GetStatus(WS_DMA_HW,WS_DMA_CHANNEL) & CY_DMA_INTR_CAUSE_COMPLETION))
{
WS_DMATrigger();
}
}

From lines 156-372 I use the original functions to implement the frame buffer for WS2812 (you can read about that in the original article).  I am not including these functions here.

The final block of code is the actual task which manages the ws2812 led string.  On lines 379->395 it sets up the SPI, DMA, Queue and Timer.   Then it goes into the infinite loop waiting for command messages.  The message loop just looks at the command, the calls the correct helper function.

void ws2812Task(void *arg)
{
ws2812_msg_t msg;
cy_stc_scb_spi_context_t WS_SPI_context;
vTaskDelay(100);
printf("Starting ws2812 task\n");
WS_runTest();
WS_frameBuffer[0] = 0x00;
WS_setRange(0,ws2812_NUM_PIXELS-1,0,0,0); // Initialize everything OFF
Cy_SCB_SPI_Init(WS_SPI_HW, &WS_SPI_config, &WS_SPI_context);
Cy_SCB_SPI_Enable(WS_SPI_HW);
WS_DMAConfigure();
// This queue handles messages from the keyboard
ws2812QueueHandle = xQueueCreate( 10,sizeof(ws2812_msg_t));
// This timer calls the update function every 30ms if it is turned on.
ws2812TimerHandle = xTimerCreate("ws2812 timer",pdMS_TO_TICKS(30),pdTRUE,0,ws2812CallbackFunction );
while(1)
{
xQueueReceive(ws2812QueueHandle,&msg,0xFFFFFFFF);
switch(msg.cmd)
{
case ws2812_cmd_update:
if(!wsAutoUpdateState)
{
WS_DMATrigger();
}
break;
case ws2812_cmd_autoUpdate:
if(wsAutoUpdateState && msg.data == false)
{
xTimerStop(ws2812TimerHandle,0);
}
else if(!wsAutoUpdateState && msg.data == true)
{
xTimerStart(ws2812TimerHandle,0);
}
wsAutoUpdateState = msg.data;
break;
case ws2812_cmd_setRGB:
WS_setRGB( msg.data,msg.red,msg.green ,msg.blue);
break;
case ws2812_cmd_setRange:
WS_setRange(msg.data>>16 & 0xFFFF, msg.data&0xFFFF, msg.red,msg.green ,msg.blue);
break;
case ws2812_cmd_initMixColorRGB:
WS_initMixColorRGB();
break;
}
}
}

Update main.c to use the Public Interface of ws2812.h

Initially when I did this, I just updated main.c.  But, after thinking about it a little bit I decided that it was better to create a uartTask.h and uartTask.c to make the keyboard processing a bit more self contained.  Starting with the public interface to uartTask.h.  This file simply declares the function prototype for the uartTask.

/*
* uartTask.h
*
*  Created on: Jun 16, 2019
*      Author: arh
*/
#ifndef SOURCE_UARTTASK_H_
#define SOURCE_UARTTASK_H_
void uartTask(void *arg);
#endif /* SOURCE_UARTTASK_H_ */

I do not like to poll!  Ever!  That is the point of an RTOS.  Don’t poll if you can at all get away from it.  To avoid polling I set up the SCB UART to give an interrupt when it receives a character.  In the ISR I then turn off the interrupts and increment a semaphore.  In the main body of the task I “sit” on the semaphore and wait for it to be incremented.  Once it is incremented, I read and process characters until there are no more.  Then turn the interrupts back on.

The uartTask.c has three sections

  • The header where I do all of the includes and define the semaphore
  • The ISR where I turn off the interrupts and set the semaphore
  • The main task.

First, the beginning of the file just does the normal includes.  It also declares a context for the UART and it declares a handle for the semaphore.

/*
* uartTask.c
*
*  Created on: Jun 16, 2019
*      Author: arh
*/
#include <stdio.h>
#include "ws2812.h"
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
#include "cy_device_headers.h"
#include "cycfg.h"
#include "cy_pdl.h"
cy_stc_scb_uart_context_t UART_STDIO_context;
SemaphoreHandle_t UART_STDIO_SemaphoreHandle;

The ISR simply turns off the interrupt mask so that no interrupts happen until the Rx fifo is clear (line 24).  Then clears the interrupt source (meaning tells the SCB to turn off the interrupt) so that it doesn’t just re-pend the interrupt (line 25).  Then it increments the semaphore and does the normal FreeRTOS context switch if needed.

void UART_Isr(void)
{
// Disable & clear the interrupt
Cy_SCB_SetRxInterruptMask(UART_STDIO_HW,0);
Cy_SCB_ClearRxInterrupt(UART_STDIO_HW, CY_SCB_RX_INTR_NOT_EMPTY);
static BaseType_t xHigherPriorityTaskWoken;
xHigherPriorityTaskWoken = pdFALSE;
xSemaphoreGiveFromISR( UART_STDIO_SemaphoreHandle, &xHigherPriorityTaskWoken );
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}

The uartTask function has several parts

  • Initialize the semaphore (line 36)
  • Initialize the SCB and Interrupt (lines 38-49)
  • Waits for the semaphore to be set (line 55)
  • Loops until the Rx FIFO is empty (line 57)
  • Reads a character and does correct operation with a giant switch (59-127)
  • When all the characters are done being read (aka the Rx FIFO is empty) turn back on the interrupt (line 129)
void uartTask(void *arg)
{
UART_STDIO_SemaphoreHandle = xSemaphoreCreateCounting( 0xFFFF,0); // Semaphore counts unprocessed key presses
Cy_SCB_UART_Init(UART_STDIO_HW,&UART_STDIO_config,&UART_STDIO_context);
cy_stc_sysint_t uartIntrConfig =
{
.intrSrc      = UART_STDIO_IRQ,
.intrPriority = 7,
};
(void) Cy_SysInt_Init(&uartIntrConfig, &UART_Isr);
NVIC_EnableIRQ(UART_STDIO_IRQ);
Cy_SCB_SetRxInterruptMask(UART_STDIO_HW,CY_SCB_RX_INTR_NOT_EMPTY);
setvbuf( stdin, NULL, _IONBF, 0 ); // Turn off Input buffering on STDIO
Cy_SCB_UART_Enable(UART_STDIO_HW);
printf("Starting UART Task\n");
for(;;)
{
xSemaphoreTake( UART_STDIO_SemaphoreHandle, 0xFFFFFFFF); // Wait for a semaphore
while(Cy_SCB_UART_GetNumInRxFifo(UART_STDIO_HW))
{
char c=getchar();
switch(c)
{
case 'u':
printf("Enable auto DMA updating\n");
ws2812_autoUpdate(true);
break;
case 'U':
printf("Disable auto DMA updating\n");
ws2812_autoUpdate(false);
break;
case 't':
printf("Update LEDs\n");
ws2812_update();
break;
case 'r':
ws2812_setRGB(0,0xFF,0,0);
printf("Set LED0 Red\n");
break;
case 'g':
ws2812_setRGB(0,0,0xFF,0);
printf("Set LED0 Green\n");
break;
case 'O':
ws2812_setRange(0,ws2812_NUM_PIXELS-1,0,0,0);
printf("Turn off all LEDs\n");
break;
case 'o':
ws2812_setRange(0,ws2812_NUM_PIXELS-1,0xFF,0xFF,0xFF);
printf("Turn on all LEDs\n");
break;
case 'b':
ws2812_setRGB(0,0,0,0xFF);
printf("Set LED0 Blue\n");
break;
case 'R':
ws2812_setRange(0,ws2812_NUM_PIXELS-1,0x80,0,0);
printf("Turn on all LEDs RED\n");
break;
case 'G':
ws2812_setRange(0,ws2812_NUM_PIXELS-1,0,0x80,0);
printf("Turn on all LEDs Green\n");
break;
case 'B':
ws2812_setRange(0,ws2812_NUM_PIXELS-1,0,0,0x80);
printf("Turn on all LEDs Blue\n");
break;
case 'a':
ws2812_initMixColorRGB();
printf("Turn on all LEDs RGB Pattern\n");
break;
case '?':
printf("u\tEnable Auto Update of LEDs\n");
printf("U\tDisable Auto Update of LEDs\n");
printf("t\tTrigger the DMA\n");
printf("r\tSet the first pixel Red\n");
printf("g\tSet the first pixel Green\n");
printf("b\tSet the first pixel Blue\n");
printf("O\tTurn off all of the pixels\n");
printf("o\tSet the pixels to white full on\n");
printf("R\tSet all of the pixels to Red\n");
printf("G\tSet all of the pixels to Green\n");
printf("B\tSet all of the pixels to Blue\n");
printf("a\tSet pixels to repeating RGBRGB\n");
printf("?\tHelp\n");
break;
}
}
// turn the rx fifo interrupt back on
Cy_SCB_SetRxInterruptMask(UART_STDIO_HW,CY_SCB_RX_INTR_NOT_EMPTY); // Turn on interrupts for Rx buffer
}
}

Rewire to Use a Level Shifter

At the end of the previous article I said “I’m Lucky it Works. The last thing to observe in all of this is that I am driving the LED string with a 5V wall wart. And according to the datasheet VIH is 0x7 * VDD = 3.5V … and I am driving it with a PSoC 6 with 3.3V. Oh well.”  This time I am not so lucky.  I am not totally sure why (probably because I used a different power supply) but it doesn’t work.  So I put my lab assistant to work putting together a level shifter that I got from SparkFun.  For those of you long time readers, you will say, “Hey that isn’t Nicholas”.  Well, it is my other lab assistant, Anna.  And she is just as good at soldering!

Now when I try it, everything works!

What is next?

As I was working on the project, I thought of several things that I would like to add to the project including:

  • A random color / blinking mode
  • A CapSense button and slider
  • The TFT display
  • Ability to handle multiple strips of LEDs

But for now, all that stuff is for another day.

You can find all of this code at my github site. git@github.com:iotexpert/WS2812-MTB.git

IoT Expert Logo -> EPD – Bitmap Madness (Part 2)

Summary

In the last article I showed you how to display a bitmap in the Segger format.  In this article I will show you how to convert a BMP to a “C” file and then display it using the Segger GUI_BMP_Draw() API.

To this I will follow these steps:

  • Use GIMP to Export a Black and White Logo
  • Convert the BMP to a “C” array using Bin2C
  • Add the C to the project and fix up the project

Use GIMP to Export a Black and White Logo as BMP

As in the last article, Ill use GIMP to manipulate the IoT Expert.  First, Ill load the logo from the PNG file.

Then I will convert the PNG to a black and white only image.  This will be a 1-bit per pixel indexed image.  Indexed means that instead of ARGB (aka 32 bits for each pixel), that the color of each pixel will be referenced via an index into a color table.  For instance the color table might look like this:

Index Value Color
0 0xFF000000 Black
1 0xFFFFFFFF White

The BMP file will then have a color table (like the one above) and each pixel value will be an index into the table.  It turns out that these color tables must have  2^n rows where n is the number of bits in the index.  Also with the BMP format any bitmaps with 24 bits per pixel will not have a color table.

Gimp can convert an image to indexed.  To do this, click  Image–>Mode–>Indexed…

On this screen you will be given the option to specify the color indexes.  Notice that there is a “black and white only” option.

Once you have made the conversion you can see that image is now indexed.

On that screen you could have converted the image to indexed with up to 256 colors (but I choose 2 by using the Use black and white (1-bit) palette.

The next step is to export to a BMP using “File–>Export As…”

I set the file name to “IOTexpert_Logo_Vertical_BW.bmp”.  Gimp does its magic by using the file extension.  In this case “bmp” creates a Microsoft BMP file.

When I hit export I get this dialog box (which I had no idea what it meant when I started). For now click “OK”

Once the file is exported this is what I get.  OK… is not very helpful.  What happened?

The answer is that an Indexed BMP does not support Alpha.  So, GIMP ditched the Alpha, which turned everything that wasnt black to the background color, which is black. So, what is the deal with the Alpha channel?  Alpha is how transparent everything is.  You can get rid of it using Layer–>Transparency–>Remove Alpha Channel

Which once again turns my image black.  But why?  On the left hand side of the screen you will see the foreground/background colors.  And you will notice that the background is black.

To fix the problem undo the remove alpha.  Then click the little back and forth arrow to turn the background white.  Then re-remove the alpha.  Now that problem is fixed.

Now, I can shrink it to the right size using Image–>Scale Image…

Then pick the 276 width (aka the same width as the EPD screen)

Now do “File–>Export As..” with a “.bmp” file name extension.  This time it doesn’t ask me about the transparency.

And, now I have a nice BMP file.  Here is the view from the preview in Mac.

So, how do I get a BMP file into my program?  Well, I need turn it into an array of bytes.  And to do that…

Segger Bin2C

One of the utility programs that Segger provides is called “Bin2C” which can read in a file and turn it into an array of bytes in “c” format.  You can download it here.

When I run it, first I select the file, then press “Convert”

And it generates a nice array of bytes.

Update the Project

To use the array, first copy the file into your project.  You notice that the array is defined as “static” which means that it is not accessible from other files.  Remove that.  Now edit the eInkTask.c and

  1. Add an extern reference to the array of bytes
  2. Make a call to “GMP_BMP_Draw()” to display the logo
extern unsigned char _acIOTexpert_Logo_Vertical_BW[6862UL + 1];
void ShowIoTScreen(void)
{
GUI_Clear();
GUI_BMP_Draw(_acIOTexpert_Logo_Vertical_BW, 0,0);
/* Send the display buffer data to display*/
UpdateDisplay(CY_EINK_FULL_4STAGE, true);
while(1)
vTaskDelay(100);
}

When I program the kit I get this… all black.

 

But why?  I didn’t know the answer.  So I assumed that it must be something to do with me and my understanding of bitmaps.  In the next article I’ll tell you all about that journey.  But after a day or two of learning about bitmap file formats I was convinced that it wasn’t me.  So I started looking around on the web and I found this thread on Segger’s forum.

And, when I got to work the next Monday I called an amazing engineer that works for Cypress in Ukraine. He provided me a v5.48 which sure enough fixed the problem.  When I program that, looks like things are working with bitmaps:

Unfortunately that means that we (Cypress) have a released version of Segger emWin that is broken.  This will be fixed with an updated version soon, but for now if you are stuck send me an email and I’ll help you.

The next article is a deep dive into the BMP format.

IoT Expert Logo –> EPD – Bitmap Madness (Part 1)

Summary

In the last article I showed you a bunch of things about programming the Pervasive EPD eInk Display that is attached to the CY8CKIT-028-EPD.  You might have noticed in the first video I have a screen that shows the IoT Expert Logo.  Simple right?  Yes you would think, but it actually turned out to be quite a pain in the ass!  This article is my journey through bit maps.  It is hardly canonical, but hopefully it will help you.

In this article I will specifically walk you through:

  • The IoT Expert Logo
  • Segger emWin Bitmap Drawing APIs
  • Segger Bitmap Converter
  • Updating a Project to draw a Segger Bitmaps
  • Converting a Color Bitmap to Black and White
  • Using GIMP to Fix B/W Conversion

In the next two articles I will address drawing bitmaps that are in the Windows BMP format and PNG format.

The IoT Expert Logo

If you guys remember, in early 2017, I ran a design contest to create a logo for the IoT Expert website.  You can read about it here and here.  When it was over, I had a bunch of different images including this one which is a 1091×739 PNG file with what I thought was five colors but is actually nine (which I discovered during this journey)

OK, thats cool.  But how do I get that onto the eInk screen which is 276×176 and black and white?

emWin Bitmap Drawing APIs

I started by looking at the Segger emWin documentation which you can either get directly from the Segger website here.  Or you can find it inside of Modus Toolbox.  Select “Help–>ModusToolbox API Reference–>PSoC PDL Reference”

Then pick “Middleware and Software API Reference –> Segger emWin –> emWin User Guide”

From the documentation you see that emWin can display bitmaps in the emWin format using the APIs GUI_DrawBitmap.  This section actually goes on for more than another page worth of APIs.  The API that I will focus on in this article is GUI_DrawBitmap()

You can also display bitmaps that are in GIF, PNG, BMP or JPEG format.

Bitmap Converter for emWin

I suppose the first question is, “How do I get a bitmap from my computer in PNG format into the Segger Bitmap format?”  Well, it turns out that Segger has a program called Bitmap Converter for emWin.

This is a pay program, but you can download it to try it out.  It is sort of an old-school windows program.  So I installed it on parallels on my mac.  When you run it the first time it reminds me that this is not for production.  Got it!

I start by opening the PNG file of my logo.  Notice that it says the file is 1091 by 739 and in “ARGB” colors.  “ARGB” means Alpha, Red, Green and Blue. (more on this later).

On the Image menu I start by picking “Scale..” to reduce the size.

I pick out 276 wide and it keeps the aspect ratio the same, which results in a height of 186 (actually 10 pixels to high)

After clicking OK I get this.

Now, I want to take that bitmap and turn it into a “C” file that has the right data structures.  To do that pick “Save As..”

Then pick “C” bitmap file (*.c)

Now, it asks me this question, which I didn’t really know the answer to. (more on this later) but I let the default be “True color with alpha”

This created a “C” file called IOTexpert_Logo_Vertical.c” which seems to be OK.

Updating a Project to draw a Segger Bitmap

Rather than make a new project.  I start with the project from the previous article.  I use the finder to copy/paste the c file into my project.  You can see it below.

Then I double click on the file.  Here is the top.  Notice it reminds me that this is demo only.  And it gives me a little bit of information about the bitmap.  Specifically the width and height.  As well as the number of colors which is 32 bits per pixel.  It turns out that this is 4-bytes per pixel.  The first byte is Alpha and then one byte each for Red, Green and Blue.  Notice that it also declares an extern structure “extern GUI_CONST_STORAGE GUI_BITMAP bmIOTexpert_Logo_Vertical”.  This is exactly the right type to call the GUI_Drawbitmap function.

/*********************************************************************
*                SEGGER Microcontroller GmbH & Co. KG                *
*        Solutions for real time microcontroller applications        *
*                           www.segger.com                           *
**********************************************************************
*                                                                    *
* C-file generated by                                                *
*                                                                    *
*        Bitmap Converter for emWin (Demo version) V5.48.            *
*        Compiled Jun 12 2018, 15:10:41                              *
*                                                                    *
*        (c) 1998 - 2018 Segger Microcontroller GmbH                 *
*                                                                    *
*        May not be used in a product                                *
*                                                                    *
**********************************************************************
*                                                                    *
* Source file: IOTexpert_Logo_Vertical                               *
* Dimensions:  276 * 186                                             *
* NumColors:   32bpp: 16777216 + 256                                 *
*                                                                    *
**********************************************************************
*/
#include <stdlib.h>
#include "GUI.h"
#ifndef GUI_CONST_STORAGE
#define GUI_CONST_STORAGE const
#endif
extern GUI_CONST_STORAGE GUI_BITMAP bmIOTexpert_Logo_Vertical;
static GUI_CONST_STORAGE U32 _acIOTexpert_Logo_Vertical[] = {
0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 

In my project I create a new function called “ShowIoT” screen.  This will just clear the screen, update the display, then draw the bitmap, then update the screen, then wait forever.  In order for my file to know about the bitmap I copy the “extern GUI_CONST_STORAGE GUI_BITMAP bmIOTexpert_Logo_Vertical” into my file.  Typically this declaration would be in a “.h” file that was paired with the “.c” file.  Oh well.

extern GUI_CONST_STORAGE GUI_BITMAP bmCypressLogoFullColor_PNG_1bpp;
void ShowIoTScreen(void)
{
/* Set foreground and background color and font size */
GUI_Clear();
GUI_SetBkColor(GUI_WHITE);
GUI_SetColor(GUI_BLACK);
UpdateDisplay(CY_EINK_FULL_4STAGE, true);
GUI_DrawBitmap(&bmCypressLogoFullColor_PNG_1bpp, 0, 0);
/* Send the display buffer data to display*/
UpdateDisplay(CY_EINK_FULL_4STAGE, true);
while(1)
vTaskDelay(100);
}

When I build the project I find out.. HOLY CRAP my project is now 270648 bytes.  Wow.

=========================================
== Application CM0+ Memory ==
=========================================
code:6560	sram:1724
=========================================
== Application CM4 Memory ==
=========================================
code:270648	sram:278508

Why is this?  Simple, by looking at the linker map you can see that the array of data for the bitmap is 0x32220 which is also known as 205344 bytes.  Im going to have to figure out something better than that.

 .rodata._acIOTexpert_Logo_Vertical
0x0000000000000000    0x32220 ./Source/IOTexpert_Logo_Vertical.o

When I program the screen I get this… which obviously is jacked up.

But what to do?

Converting a Color Bitmap to Black and White

Well instead of a color image (32 bits-per-pixel) let’s use the Bitmap Converter for emWin (Demo version V5.8 to convert the image to BW.  On the Image –>Covert to –> BW (1BPP)

After running that I get this. (what happened to my logo?).


After exporting the new image to a “.c” file I go have a look.  OK it isnt very often that I learn something new about “C”.  But look at this.  Apparently you can represent binary data as “X” and “_” when initializing arrays.  Who knew?

static GUI_CONST_STORAGE unsigned char _acIOTexpert_Logo_Vertical[] = {
XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, 
XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXXXXXX, XXXX____,

When I build the project I find that it is much much smaller.  Thats good.

=========================================
== Application CM0+ Memory ==
=========================================
code:6560	sram:1724
=========================================
== Application CM4 Memory ==
=========================================
code:69744	sram:278492

And I find that the image occupies 0x196e bytes (also known as 6510 in decimal).  Much better.

.rodata._acIOTexpert_Logo_Vertical
0x000000001000f880     0x196e ./Source/IOTexpert_Logo_Vertical.o

But, when I program the board, my image is jacked up.  I suppose that I shouldn’t be surprised as thats what the program showed me as well.

Using GIMP to Fix B/W Conversion

My lab assistant, Nicholas, looked at the image and said.  The problem is that when you converted it to black and white, the light colors in the logo turned to white instead of black.  OK.  How do I fix it?  Simple, install GIMP and edit the PNG.  GIMP is GNU Image Processor and is a program that acts like Adobe Photoshop.

Start by opening up the logo and it tell me nearly the same thing as the BitMap converter program.

On the left side of the screen there is a “bucket” icon which will pour color into regions of the image.  So, to make things work I pour black everywhere there is color.  That little black white thing specifies the foreground and background colors.

Now I take the file and export it back to an PNG.

When you pick “PNG” you need to give it some options.  Which I took also as default.

Now when I open it up in the Bitmap Converter it looks all black and white.  BUT notice that it is still “ARGB”

So, I convert it to black and white.

Then I follow the same process to program the development kit. (export C file, copy into project, fix up the extern and build/program).  Excellent.  Now my image is good.

In the next article I will talk more about the Bitmap format, and colors, and Alpha.  I will then show you how to use some of the other APIs.

CY8CKIT-028-EPD: How Does The Driver Work?

Summary

Before I finish this series there are two more issues which I would like to address.  First, I want to walk you through the schematic and show you how things are connected.  And second, I want to talk about the “Update Scheme”.  Unfortunately, there are a couple of other things that I would like to dig into, but for now this article will be the last.  But, I will leave a few links at the end of the article which will give you a hint about other things that I might be interested in.

Electrical Interface

If you follow back through the previous articles you will notice that there are several different pins.  Here is the pin assignment from PSoC Creator.

But what do they do?  If you look at the list you will see that four of them are to control the SPI interface to the G2 display driver. (miso, mosi, sclk, CY_EINK_Ssel).  The rest of them Ill go one by one through.

First is the pin called “CY_EINK_DispEn”.  This pin really should have been called “DISP_PWR_EN” so that it matched the actual shield schematic.  This is a digital output pin which is connected to a Vishay sip32401a 1.1 V to 5.5 V, Slew Rate Controlled Load Switch.  Simply a power switch for the display.  Notice in the schematic that there is a 100K pulldown resistor connected to the enable which means that by default the power is off to the display.  Also notice that R3 is a “No Load” pullup resistor.  You could remove R4 and load R3 to make the power on by default… which I don’t think that you would actually ever do as if you are using an EPD you probably care about power.

The next pin is called “CY_EINK_DispIoEn”.  This is a digital output pin which is connected to “DISP_IO_EN_L” on the shield.  This is simply the I/O enable of a Fairchild FXMA108BQX level shifter.  This allows the PSoC to run at lower voltages (e.g. 1.8v) than the 3.3v required by the EPD G2 driver chip.  This would also enable a chip to run at a higher voltage (e.g. 5V) if you were using a 5V capable PSoC (e.g. all of the PSoC 4s).  The schematic uses the same pullup/down scheme that was used on the power switch above.

The next pin is called “CY_EINK_Discharge” and is a digital output from the PSoC.  Notice that when the PSoC drives this pin high that it will enable two power transistors and will short “VGH” and “VDH” to ground.

If you read the “E-paper Display COG Driver Interface Timing for 1.44”,1.9”,2”,2.6” and 2.7” EPD with G2 COG and Aurora Mb Film” document you will see this note:

And a bit later on in the documented you will see this logic diagram.

According to the data sheet, Vgh is driven to >12v and Vdh>8v by a charge pump while talking to the screen.  What I don’t understand is why the note says to drive “Vdd and Vcc” to ground when their schematic says Vdh and Vgh.  I am assuming that the note is an error and the schematic is correct, but Ill send them a note and ask. [edit: I got a quick response from an excellent FAE at Pervasive… with this answer]

“No, the expression of Note 1 about Vcc/Vdd, it means the power off command set. You can also refer to Power off sequence in section 6 on page 34 of 4P018-00 as follows”

The last digital I/O pin is called “CY_EINK_Border”.  This pin is connected to the note “EPD_BRDR_CTRL” on this little circuit on the shield.

If you look in the documentation you will see this note:

And when you look at the timing diagram you see this which shows that after you have update the frame, that you need to do a low, high, low of the border to make it white again.

This transition is handled for you by the function “Pv_EINK_HardwarePowerOff” function… which I chopped out a little bit of to show the border control.

pv_eink_status_t Pv_EINK_HardwarePowerOff(void)
{
.....
/* After E-INK updates, the border color may degrade to a gray level that is not
as white as the active area. Toggle the Border pin to avoid this phenomenon. */
CY_EINK_Delay(PV_EINK_DUMMY_LINE_DELAY);
CY_EINK_BorderLow;
CY_EINK_Delay(PV_EINK_BOARDER_DELAY);
CY_EINK_BorderHigh;
...
turn of the G2    
....
/* Detach SPI and disable the load switch connected to E-INK display's Vcc */
Cy_EINK_DetachSPI();
CY_EINK_TurnOffVcc;
/* Return the pins to their default (OFF) values*/
CY_EINK_BorderLow;
CY_EINK_Delay(PV_EINK_CS_OFF_DELAY);
CY_EINK_CsLow;
CY_EINK_RstLow;
CY_EINK_DischargeHigh;
CY_EINK_Delay(PV_EINK_DETACH_DELAY);
CY_EINK_DischargeLow;
/* If all operations were completed successfully, send the corresponding flag */
return(PV_EINK_RES_OK);
}

Update Scheme

If you look at the original picture that I posted,  you can see that “Hassane…” text.  But if you look closely you can see a “ghost image” of the Cypress logo in the background.  Why is this?

It turns out that Pervasive has three schemes for updating the screen they are called

  1. Four stage
  2. Two stage
  3. Partial

The four stage update actually writes four complete images on the screen as below (here is the picture from the Pervasive document)

The purpose of this four stage update is to reduce the ghost images which remain from the previous updates.  Remember that the cool part about these screens is that there are crystals that flip from white to black and back… and once they are flipped you do not need to maintain power to keep them flipped.  The bad news is that they really want to stay flipped which causes Ghosting.

So why can you see the old image of the Cypress logo?  Simple,  when the four-stage update happened, I had just programmed the kit which means that my program had no idea what was on the screen from before.  This made stage 1 not work correctly because it had to assume all white.

The next question is what is the problem with the four-stage update?  Well it takes a while (like about 2 seconds) on the 2.7″ screen.  And because it writes 4 times it also consumes more power.  Pervasive also says that you can do a two-stage update with just stage 1 and stage 4 from above.  In my case this cuts the time in about half.

Finally you can also do a “partial” update.  I tried this and it didn’t work very well for my demo application which massively changes the screen from screen to screen.  But, it does seem to work pretty well for a series of updates to the same reigon (like this counter).  Here is a video I made showing Partial, Two and Four stage updates.   In addition our API lets you turn the power on/off for the G2 Driver – called “power cycle”.  I used that as a variable as well.

Terms of Art

EPD – Electrophoretic Display

eTC – external timing control

iTC – internal timing control

G2 COG – Display Controller Chip… Chip on Glass

FPL – Front Plane Laminate (of which Aurora ma and mb are two types)

Aurora ma – Wide Temperature film

Aurora mb – Low power

E2271CS021 – Aurora mb 2.71″ EPD Panel – on CY8CKIT-028-EPD

E2271BS021 – Aurora ma 2.71″ EPD Panel

References

mbed add http://os.mbed.com/users/dreschpe/code/EaEpaper/

http://www.pervasivedisplays.com/kits/ext2_kit

https://www.nayuki.io/page/pervasive-displays-epaper-panel-hardware-driver

https://github.com/nayuki/Pervasive-Displays-epaper-driver

https://github.com/repaper/gratis

https://github.com/aerialist/repaper_companion

https://www.paulschow.com/2017/02/pervasive-displays-epd-extension-kit.html

https://embeddedcomputing.weebly.com/pervasive-displays-e-paper-epd-extension-kit-gen-2.html

CY8CKIT-028-EPD Better Timing

Summary

In the first article of this series I talked about how to make the CY8CKIT-028-EPD EINK Shield work with PSoC 6 and Modus Toolbox 1.1. In the second article I improved the interface and talked about the PSoC 6 clocking system.  In this article I want to address the timing system in the EINK firmware.  You might recall that I used one of the Timer-Counter-Pulse-Width-Modulator blocks a.k.a the TCPWM inside of the PSoC 6 as a Timer for updating the EINK Screen.  Using this timer was a bit of a waste as the CM4 already has a timer built into the device called the SysTick timer.  Moreover, the SysTick timer is connected to the FreeRTOS timing system which provides you APIs to talk to it.  For this article I will talk about:

  • ARM SysTick
  • Cypress PDL and SysTick
  • FreeRTOS and SysTick
  • Make a new project & copy the files
  • Use the FreeRTOS timing system to measure the speed increase of the updated SPI
  • Remove the hardware timer & replace with the RTOS timer.

ARM SysTick

The ARM Cortex-M MCUs have an option to include a 24-bit timer called SysTick.  As best I can tell, every MCU maker always chooses to have the SysTick option built in.   Certainly the PSoC 4 and PSoC 6 family all have it built in.   But how do you talk to it?  Well, my buddy Reinhard Keil decided that it was silly for everyone to create a different method for interacting with standard ARM peripherals so he created the Cortex Microcontroller Software Interface Standard (CMSIS)

CMSIS defines two things that you need to do to make the SysTick timer work.  First, you need to create a function called EXACTLY “SysTick_Handler”.  This function gets loaded into the vector table of your program as the interrupt handler for the SysTick interrupt.  As such the function prototype is “void SysTick_Handler(void)”.  The second thing that you need to do is initialize how often the timer should be called.  You do this with the CMSIS call:

SysTick_Config(SystemCoreClock/1000);

It is interesting to note that the symbol SystemCoreClock is also defined by CMSIS as the frequency of the clock.  So the above call would setup the SysTick to be called every 1Ms (that is why there is a divide by 1000).

Here is an example I created starting with the BlinkyLED example project.  After I created the project, I added the kitprog uart (which is SCB5) and I added the Retarget I/O middleware.

#include "cy_pdl.h"
#include "cycfg.h"
#include <stdio.h>
volatile uint32_t count;
void SysTick_Handler(void)
{
count += 1;
}
cy_stc_scb_uart_context_t kitprog_context;
int main(void)
{
Cy_SCB_UART_Init(kitprog_HW,&kitprog_config,&kitprog_context);
Cy_SCB_UART_Enable(kitprog_HW);
/* Set up internal routing, pins, and clock-to-peripheral connections */
init_cycfg_all();
SysTick_Config(SystemCoreClock/1000);
/* enable interrupts */
__enable_irq();
for (;;)
{
printf("Test count=%d\n",(int)count);
Cy_GPIO_Inv(LED_RED_PORT, LED_RED_PIN); /* toggle the pin */
Cy_SysLib_Delay(1000/*msec*/);
}
}

Don’t forget to setup the standard i/o by modifying stdio_user.h

#include "cycfg.h"
/* Must remain uncommented to use this utility */
#define IO_STDOUT_ENABLE
#define IO_STDIN_ENABLE
#define IO_STDOUT_UART      kitprog_HW
#define IO_STDIN_UART       kitprog_HW

When you run the program above you should get something like this:

One interesting question is HOW does the function SysTick_Handler get into the vector table?  Well if you run an eclipse search (type ctrl-h)

You will find it in an assembly language file called “startup_psoc6_01_cm4.s”

Double click on the file and you can see the Vector table.

__Vectors:
.long    __StackTop            /* Top of Stack */
.long    Reset_Handler         /* Reset Handler */
.long    CY_NMI_HANLDER_ADDR   /* NMI Handler */
.long    HardFault_Handler     /* Hard Fault Handler */
.long    MemManage_Handler     /* MPU Fault Handler */
.long    BusFault_Handler      /* Bus Fault Handler */
.long    UsageFault_Handler    /* Usage Fault Handler */
.long    0                     /* Reserved */
.long    0                     /* Reserved */
.long    0                     /* Reserved */
.long    0                     /* Reserved */
.long    SVC_Handler           /* SVCall Handler */
.long    DebugMon_Handler      /* Debug Monitor Handler */
.long    0                     /* Reserved */
.long    PendSV_Handler        /* PendSV Handler */
.long    SysTick_Handler       /* SysTick Handler */

But how do the _Vectors get into the right place?  Well? run the search again and you will find that the linker script (which Cypress created) for your project has the definition.

When you look in the linker script you can see that it is installed at the top of the flash

    {
. = ALIGN(4);
__Vectors = . ;
KEEP(*(.vectors))
. = ALIGN(4);
__Vectors_End = .;
__Vectors_Size = __Vectors_End - __Vectors;
__end__ = .;
. = ALIGN(4);
*(.text*)
KEEP(*(.init))
KEEP(*(.fini))
/* .ctors */
*crtbegin.o(.ctors)
*crtbegin?.o(.ctors)
*(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors)
*(SORT(.ctors.*))
*(.ctors)
/* .dtors */
*crtbegin.o(.dtors)
*crtbegin?.o(.dtors)
*(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors)
*(SORT(.dtors.*))
*(.dtors)
/* Read-only code (constants). */
*(.rodata .rodata.* .constdata .constdata.* .conststring .conststring.*)
KEEP(*(.eh_frame*))
} > flash

And the CM4 flash is defined to start at 0x100002000

MEMORY
{
/* The ram and flash regions control RAM and flash memory allocation for the CM4 core.
* You can change the memory allocation by editing the 'ram' and 'flash' regions.
* Note that 2 KB of RAM (at the end of the RAM section) are reserved for system use.
* Using this memory region for other purposes will lead to unexpected behavior.
* Your changes must be aligned with the corresponding memory regions for CM0+ core in 'xx_cm0plus.ld',
* where 'xx' is the device group; for example, 'cy8c6xx7_cm0plus.ld'.
*/
ram               (rwx)   : ORIGIN = 0x08002000, LENGTH = 0x45800
flash             (rx)    : ORIGIN = 0x10002000, LENGTH = 0xFE000
/* This is a 32K flash region used for EEPROM emulation. This region can also be used as the general purpose flash.
* You can assign sections to this memory region for only one of the cores.
* Note some middleware (e.g. BLE, Emulated EEPROM) can place their data into this memory region.
* Therefore, repurposing this memory region will prevent such middleware from operation.
*/
em_eeprom         (rx)    : ORIGIN = 0x14000000, LENGTH = 0x8000       /*  32 KB */
/* The following regions define device specific memory regions and must not be changed. */
sflash_user_data  (rx)    : ORIGIN = 0x16000800, LENGTH = 0x800        /* Supervisory flash: User data */
sflash_nar        (rx)    : ORIGIN = 0x16001A00, LENGTH = 0x200        /* Supervisory flash: Normal Access Restrictions (NAR) */
sflash_public_key (rx)    : ORIGIN = 0x16005A00, LENGTH = 0xC00        /* Supervisory flash: Public Key */
sflash_toc_2      (rx)    : ORIGIN = 0x16007C00, LENGTH = 0x200        /* Supervisory flash: Table of Content # 2 */
sflash_rtoc_2     (rx)    : ORIGIN = 0x16007E00, LENGTH = 0x200        /* Supervisory flash: Table of Content # 2 Copy */
xip               (rx)    : ORIGIN = 0x18000000, LENGTH = 0x8000000    /* 128 MB */
efuse             (r)     : ORIGIN = 0x90700000, LENGTH = 0x100000     /*   1 MB */
}

And when you look at the linker MAP file which is in your project Debug/BlinkyLED_mainapp.map you will see that the vectors end up in the right place.

.text           0x0000000010002000     0x5de4
0x0000000010002000                . = ALIGN (0x4)
0x0000000010002000                __Vectors = .

Cypress SysTick

Now if you happen to be reading the PDL documentation on Saturday afternoon you might notice that there is a section of the documentation called “SysTick”.  And when you click it you will find this:

And you might ask yourself “What the hell.. those aren’t CMSIS functions?”  Well in typical Cypress fashion we created an extension to SystTick.  It does two basic things

  1. Lets you pick different clock sources for the SysTick timer
  2. Lets you setup multiple callbacks to make it easier to trigger multiple functions in your system

For this example I modified the previous project by commenting out the CMSIS calls.  And I use the Cy_SysTick calls.

#include "cy_pdl.h"
#include "cycfg.h"
#include <stdio.h>
volatile uint32_t count;
cy_stc_scb_uart_context_t kitprog_context;
#if 0
void SysTick_Handler(void)
{
count += 1;
}
#endif
void MyHander(void)
{
count += 1;
}
int main(void)
{
Cy_SCB_UART_Init(kitprog_HW,&kitprog_config,&kitprog_context);
Cy_SCB_UART_Enable(kitprog_HW);
/* Set up internal routing, pins, and clock-to-peripheral connections */
init_cycfg_all();
Cy_SysTick_Init ( CY_SYSTICK_CLOCK_SOURCE_CLK_CPU, 100000000/1000); // CPU Freq divide by 1000 makes MS
Cy_SysTick_SetCallback(0,MyHander); // Slot 0
Cy_SysTick_Enable();
//    SysTick_Config(SystemCoreClock/1000);
/* enable interrupts */
__enable_irq();
for (;;)
{
printf("Test count=%d\n",(int)count);
Cy_GPIO_Inv(LED_RED_PORT, LED_RED_PIN); /* toggle the pin */
Cy_SysLib_Delay(1000/*msec*/);
}
}

When you look at this program you might ask where I got the “100000000/1000″…. and if Hassane is reading he will ask WHY DIDN’T YOU COMMENT IT.   The answer to the first question is that it is the CPU Frequency divided by 1000 to get a millisecond timer.

As to the second question… the answer is … “I just did” 🙂

There is probably some MACRO for those values… but I just don’t know what they are… and I suppose that I should go look… but…

And finally the “// slot 0”  means that it uses the first of 5 slots… in other words places where you can store a callback.

FreeRTOS usage of SysTick

The FreeRTOS by default uses the SysTick timer to cause the scheduler to run.  And it does this by using the CMSIS interface… well because everyone needs to do their own thing, it actually lets you define the function.  Here is a clip out of FreeRTOSConfig.h where it defines the actual function name as xPortSysTickHandler.

/* Definitions that map the FreeRTOS port interrupt handlers to their CMSIS
standard names - or at least those used in the unmodified vector table. */
#define vPortSVCHandler     SVC_Handler
#define xPortPendSVHandler  PendSV_Handler
#define xPortSysTickHandler SysTick_Handler

And when you look around (using find) you will find it in the file port.c.

void xPortSysTickHandler( void )
{
/* The SysTick runs at the lowest interrupt priority, so when this interrupt
executes all interrupts must be unmasked.  There is therefore no need to
save and then restore the interrupt mask value as its value is already
known. */
portDISABLE_INTERRUPTS();
{
/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE )
{
/* A context switch is required.  Context switching is performed in
the PendSV interrupt.  Pend the PendSV interrupt. */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
portENABLE_INTERRUPTS();
}

And if you look in vTaskStartScheduler you will find that it calls the function vPortSetupTimerInterrupt where it sets up interrupt manually.

/*
* Setup the systick timer to generate the tick interrupts at the required
* frequency.
*/
__attribute__(( weak )) void vPortSetupTimerInterrupt( void )
{
/* Calculate the constants required to configure the tick interrupt. */
#if( configUSE_TICKLESS_IDLE == 1 )
{
ulTimerCountsForOneTick = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ );
xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;
ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
}
#endif /* configUSE_TICKLESS_IDLE */
/* Stop and clear the SysTick. */
portNVIC_SYSTICK_CTRL_REG = 0UL;
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
/* Configure SysTick to interrupt at the requested rate. */
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}

And what is really cool is that when you look in FreeRTOSConfig.h you can see that it uses the CMSIS macro “SystemCoreClock” and that it is configured to have a 1MS callback.

#define configCPU_CLOCK_HZ                      SystemCoreClock
#define configTICK_RATE_HZ                      1000u

So, why did I look at all of that?  Well simple, each time that the SysTick interrupt is called, the FreeRTOS adds 1 to a count…. which you can get access to by calling “xTaskGetTickCount”.  Nice.

I think that is enough background… so let’s:

Make a New Project

I want to start by creating a copy of the project from the previous article (so that alls yall can see the progression of code changes).  In the previous article I walked you step-by-step through creating and copying a project.  Here is a summary of the step you need to take.  If you want to see the details please look at the last article.

  1. Make a new project
  2. Copy design.modus
  3. Add the middleware (FreeRTOS, Segger Core OS NoTouch & Soft FP,Segger BitPlains, Retarget I/O)
  4. Copy all of the files from the source directory
  5. Update the Include paths with the “eInk Library” and “emWin_Config”

After making all of these changes I will have a project in my workspace called “EHKEinkTiming”.  I would recommend before you go further that you build and program to make sure that everything is still working.

Measure the SPI Speed Increase

All of the action to dump the frame buffer onto the EINK display happens in the function UpdateDisplay in the file eInkTask.c.  In the code below you can see that I ask FreeRTOS what the count is before I dump the display, then what the count is after it is done.

void UpdateDisplay(cy_eink_update_t updateMethod, bool powerCycle)
{
/* Copy the EmWin display buffer to imageBuffer*/
LCD_CopyDisplayBuffer(imageBuffer, CY_EINK_FRAME_SIZE);
uint32_t startCount = xTaskGetTickCount();
/* Update the EInk display */
Cy_EINK_ShowFrame(imageBufferCache, imageBuffer, updateMethod, powerCycle);
uint32_t endCount = xTaskGetTickCount();
printf("Update Display Time = %d\n",(int)(endCount - startCount));
/* Copy the EmWin display buffer to the imageBuffer cache*/
LCD_CopyDisplayBuffer(imageBufferCache, CY_EINK_FRAME_SIZE);
}

When I run the updated program I find that it takes about 1.7 seconds to update the screen.

Then I go back and modify the original program (before the SPI fixes) to see how long it takes…

And yes if you can do math, which I’m sure everyone who has read this far can, you will notice that I only sped things up by 65 Milliseconds… which means you need to call bullshit on my original declaration that it was noticeably faster.  Oh well at least I learned a bunch about the clock system.

Remove the HW timer & Update the EINK Driver

OK now that we have the hang of SysTick, it is clear that we don’t need the hardware timer that we put into the first project, so let’s get it out of there.  Start by running design.modus and removing the timer.  Just click the checkbox on “TCPWM[1]…” to turn it off.  Then press save.

If you hit compile you will find a whole bunch of errors… but they are all in four functions inside of cy_eink_psoc_interface.c.   Specifically

  • Cy_EINK_TimerInit
  • Cy_EINK_GetTimeTick
  • Cy_EINK_TimerStop

To fix them Ill first create a global static variable called “timerCount”

static uint32_t timerCount;

Then update Cy_EINK_TimerInit to just store the current FreeRTOS timer value in my new global variable.

void Cy_EINK_TimerInit(void)
{   
timerCount = xTaskGetTickCount();
}

Next update Cy_EINK_GetTimeTick to return the number of ticks since the timer was initialized.

uint32_t Cy_EINK_GetTimeTick(void)
{
/* Return the current value of time tick */
return(xTaskGetTickCount()-timerCount);
}

Finally, make the TimerStop function do… well… nothing.

void Cy_EINK_TimerStop(void)
{
}

When I build and program… my project is off to the races without the hardware timer.

In the next article Ill have a look at the EINK datasheet and driver to look into how it works.

CY8CKIT-028-EPD and Modus Toolbox 1.1

Summary

One of my very influential readers is working on a project where he wants to use the CY8CKIT-028-EPD.  But, he wants to use Modus Toolbox 1.1 instead of PSoC Creator and he observed, correctly, that Cypress doesn’t have a MTB code example project for the CY8CKIT-028-EPD.  I knew that we had a working code example in PSoC Creator (CE223727), so I decided to do a port to MTB1.1.  This turned out to be a bit of an adventure which required me to dig out a logic analyzer to solve self inflicted problems.  Here is a picture I took while sorting it out.

There are a few things in the PSoC Creator example code which I didn’t really like, so, for the final solution, I would like it to be

  • In Modus Toolbox 1.1
  • Using FreeRTOS
  • Using the Segger emWin graphics library
  • Getting the best response time
  • Using DMA to drive the display

For this article I will go through these steps:

  1. Build CE223727 EmWin_Eink_Display in PSoC Creator
  2. Explain the PSoC Creator Project
  3. Create a new MTB Project & add the FreeRTOS, Segger emWin and stdio middleware
  4. Configure the device for the correct pins, clocks and peripherals
  5. Setup FreeRTOS and Standard I/O
  6. Copy the driver files into the MTB project from the PSoC Creator workspace
  7. Port the drivers and eInkTask to work in MTB
  8. Program and Test
  9. (Part 2) Update the driver to remove the hardware timer
  10. (Part 2) Update the example to remove polled switch and use a semaphore
  11. (Part 2) Update the driver to use DMA
  12. (Part 2) Explain how the EINK EPD Display Works

If you lack patience and you just want a working project, you can download it from the IoT Expert GitHub site. git@github.com:iotexpert/eink-emwin-mtb1-1.git

First build CE223727 EmWin_Eink_Display in PSoC Creator

Start by finding the code example project for the Eink Display.  In PSoC Creator on the File->Code Example menu you will be able to pick out the code example.

There are a bunch of code examples, so the easiest way to find them is the filter based on “emwin”.  I did this because I knew we had used the Segger emWin Graphics library.  Notice in the picture below there are two emWin examples.  One with a “world” beside it and one without.  The world symbol means that it is on the internet and you will need to download it.  You can do that by clicking the world button.  Probably, you will find that your CE223727 EmWin_EInk_Display will have a world beside it and you will need to download it before you can make the project.

Once you click create project it will ask you about the project.  Just click “next”

Then give your project (and workspace) a name.  I called the workspace “EPDExample” and the project “CE22….”

After all of that is done you will have a schematic (and all of the other stuff required for the project).

When you click the program button it will ask you which MCU target to program (pick either, it doesnt matter)

After a while, your console window should look like this.

And you development kit should do its thing.

Explain the PSoC Creator Project

Now, lets have a look at the project.  Starting on the upper left hand part of the schematic you find that the interface to the EPD is via a SPI.  The SPI slave select is controlled with the Pervasive driver firmware rather than letting the SPI block directly control it.

The SPI is configured to be 16 megabits per second with CPHA=0 and CPOL=0.

I didn’t notice this at first, but in the picture above you can see that the actual speed of the SPI is 8.33 mbs.  That isn’t 16mbs for sure.  But why the gap?  The first thing to know is that in order for the SPI block to work correctly the input clock must be set at the desired datarate times the oversample.  What is oversample?  That is a scheme to get rid of glitchy-ness in the input signal.  In this case it will take 6 input samples to determine if the input is a 1 or a 0.  (median filter I think).  With this configuration the input clock to the SCB needs to be 16mbs * 6 = 96mhz.

But what is the input clock frequency?  If you click on the dwr->clocks you will see this screen which shows that the input clock is 50Mhz (the last line highlighted in blue).  Further more you can see that the source clock for the SCB is “Clk_Peri”.  When you divide 50mhz source clock rate by 6 oversample you will find that the actual bitrate is 8.33kbs.

But where does the 50mhz come from?  Well, the clock system is driven by the “IMO”.  IMO stands for internal main oscillator and it is a trimmed RC oscillator built into the chip. (thanks Tim).  This oscillator runs into an FLL which up converts it to 100MHz.

That signal is then run into the “Clk_Peri” divider which divides it by two to yield a clock of 50MHz.  Which is not all that close to 96MHz… and means that our SPI runs at the wrong speed.

But what does the EPD driver chip actually want?  You can find the documentation for this EPD on the Pervasive website.  That web page also has a link to the Product Specification 2.7″ TFT EPD Panel (E2271CS021) Rev.01 as well as the driver chip COG Driver Interface Timing for small size G2 V231

When you look in the timing document you will find that the actual chip can take up to a 20Mhz input clock.  This means that our code example actually updates the screen at 42% (8.33/20) of what it could.  That gives us a chance to make things faster… which I will do after the port to MTB.

The next sectin of the schematic has a TCPWM that is configured as a timer.  This has an input clock of 2kHz.

 

And is setup to divide by 2 which will yield a counter that updates every 1ms.  The author of this code example used the TCPWM to time operations inside of the driver (which I will also replace with something better)

Lastly there are some GPIOs that control various control pins on the display.  I don’t really know what all of the pins do, but will sort it out in the next article.

And all of the pins are assigned like this:

Create a new MTB project & Add the Middleware

It is time to start the project in MTB.  Start up Modus Toolbox 1.1 and select File->New->ModusToobox IDE Application    

Then select the CY8CKIT-062-BLE Development Kit.  This kit comes with the CY8CKIT-028-EPD EINK Shield that you can see in the pictures above.

I decide to call my project “EHKEink” and I derive my project from the “EmptyPSoC6App” template.

Once that is done, Let it rip.

And you should end up with a screen that looks like this. On the left in the workspace explorer you see the main app project.  In the middle you see the readme file which explains how this project is configured.

The next step is to add the “Middleware” that we need to make this project work.  You can do this by clicking the select Middleware button from the ModusToolbox quick panel.

For this project we need

  • FreeRTOS
  • Retarget I/O
  • Segger emWin Core, OS, no Touch, Soft FP
  • Segger emWin display driver BitPlains

The middleware selector will bring in all of the drivers you selected into your project.  You can see that it also adds the FreeRTOS configuration file “FreeRTOSConfig.h” as well as “stdio_user.c” etc.  These files endup in the source folder and are for you to edit.

While I was working on this, I found a bug in the emWin middleware, specifically the the configuration files for BitPlains get included twice.  To fix this you need to change the project properties and remove the path to “..components/psoc6mw/emWin/code/drivers/BitPlains/config”.  To do this, select the project in the workspace explorer then right click and select properties.

Then select “C/C++ General –> Paths and Symbols”.  Select the “…BitPlains/config” path and click “Delete”

Configure the device in MTB

Modus Toolbox does not have a “schematic” or a “dwr” like PSoC Creator.  In order to achieve the same functionality we built the “Configurator”.  This tool will let you setup all of the peripherals in your project.  To run it select “Configure Device” in the MTB Quick Panel.

Remember from the PSoC Creator Schematic we need to have:

  • A bunch of pins
  • A SPI
  • A Timer
  • Plus I want a UART to connect to standard I/O.

First, click on the “Pins” tab.  This lets you set all of the configuration information for each of the pins on the chip.  I will go one by one enabling the pins and setting them as digital inputs or output.  I am going to give all of the pins that exact same names that they had in the PSoC Creator Project because I know the author of that project used PDL.  When you give a pin a name in the configurator it will generate #defines or c structures based on the name.  This will make the source code the original PSoC Creator author wrote almost exactly compatible with MTB.

Here is an example of the first output pin which is P0[2] and is named CY_EINK_DispIoEn.  For the output pins you need to do four things.

  1. Enable the checkbox next to the pin name. (in this case P0[2])
  2. Give the pin a name (CY_EINK_DispIoEn)
  3. Set the drive mode (Strong Drive, Input buffer off)
  4. Set the initial state of the pin (High (1))

Now, you need to go one by one turning on all of the output pins (Im not showing you screen shots of all of them)

There are two input pins for this project SW2 P0[4] and CY_EINK_DispBusy P5[3].  For these pins I will:

  1. Enable the pin checkbox
  2. Give the pin a name (in this case SW2)
  3. Resistive Pull-Up, Input buffer on.  Note for P5[3] the pullup resistor is not needed

Now that the digital pins are configured, you can setup the STDIO Uart.  This will be used to send debugging messages to the console Uart which is attached to your computer via a USB<->UART bridge in KitProg 3.

Start by enabling SCB5 and giving it the name “UART”.  Make sure that the baud rate is set to 115200 and the rest to 8n1

Scroll down the window and pick out the RX and TX Pins plus the clock (any of the 8-bit clock dividers will do.  In this case I chose Divider 0)

Now, you need to setup the SPI.  To do this turn on SCB 6, set it to SPI, give it the name “CY_EINK_SPIM”, set it to “Master”, fix the data rate to 1000

Then scroll down to the “Connections” section and assign the pins

The last bit of hardware we need is a timer with a 1000kHz input clock, in other words a millisecond timer.  To do this start by enabling TCPWM[1] 16-bit counter.  Call it “CY_EINK_Timer” which was the same name as the PSoC Creator project.  Then setup

  • As a “Timer Counter”.
  • One shot
  • Up count
  • Period is 65535 (aka the max)
  • And pick “Clock signal” as 16 bit Divider

Given that we want it to count milliseconds and the input has a 128 bit pre-divider… we need for the input clock to be setup to 128khz.  Click on “Peripheral clocks” then select “16 Bit Divider 0”.  Notice that the input frequency is 72Mhz and we need 128Khz… to get this a divider of 562 is required.  72mhz/128khz = 562

Setup FreeRTOS and Standard I/O

The next step is to setup the “plumbing”.  In this projet we are using FreeRTOS and Standard I/O. To configure FreeRTOS just edit the “FreeRTOSConfig.h” and remove the “warning”

#warning This is a template. Modify it according to your project and remove this line. 

Enable mutexes on line 57

#define configUSE_MUTEXES                       1

Make the heap bigger on line 70

#define configTOTAL_HEAP_SIZE                   1024*48

Change the memory scheme to 4 on line 194

#define configHEAP_ALLOCATION_SCHEME                (HEAP_ALLOCATION_TYPE4)

To enable the UART to be used for Standard I/O, edit “stdio_user.h” and add the includes for “cycfg.h”.  Then update the output and input Uart to be “UART_HW” (which is the name you gave it in the configurator)

#include "cycfg.h"
/* Must remain uncommented to use this utility */
#define IO_STDOUT_ENABLE
#define IO_STDIN_ENABLE
#define IO_STDOUT_UART      UART_HW
#define IO_STDIN_UART       UART_HW

Now make a few edits to main.c to

  • Add includes for the configuration, rtos and standard i/o
  • Create a context for the UART
  • Create a blinking LED Task
  • In main start the UART and start the blinking LED task.
#include "cy_device_headers.h"
#include "cycfg.h"
#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>
cy_stc_scb_uart_context_t UART_context;
void blinkTask(void *arg)
{
(void)arg;
for(;;)
{
vTaskDelay(500);
Cy_GPIO_Inv(LED_RED_PORT,LED_RED_PIN);
printf("blink\n");
}
}
int main(void)
{
init_cycfg_all();
__enable_irq();
Cy_SCB_UART_Init(UART_HW,&UART_config,&UART_context);
Cy_SCB_UART_Enable(UART_HW);
xTaskCreate( blinkTask,"blinkTask", configMINIMAL_STACK_SIZE,  0,  1, 0  );
vTaskStartScheduler();
while(1);// Will never get here
}

As I edited the code I notice that it can’t find “LED_RED” which made me realize that I forgot to add the LED_RED attached to P0[3] in the configuration.  So, I go back and update P0[3] to be LED_RED as strong drive digital output.

Finally just to make sure that it is all working lets program the kit.  When I press “EHKEink Program” form the quickpanel…

I get this message in the console.

But how can that be?  I have my kit plugged in?  In order to program your kit using Modus you need “KitProg3”.  PSoC Creator can program you kit with KitProg3 only if it is in the CMSIS-DAP HID mode.  To switch you development kit to KitProg3, you can use the program “fw-loader” which comes with MTB.  You can see what firmware you have by running “fw-loader –device-list”.  To change to KitProg 2 run “fw-loader –update-kp2” and to update to KitProg3 run “fw-loader –update-kp3”

Now when i program I get both the LED blinking and the console printing blink.

Copy the files into the MTB project

Next, I want to bring over the drivers from the PSoC Creator project.  They reside in folder called “eInk Library” inside of the PSoC Creator project.  You can copy them by navigating to the PSoC Creator workspace, then typing ctrl-c in the File Explorer, then clicking the “Source” directory in your Eclipse WorkSpace explorer and typing ctrl-v

You will also need the four files “GUIConf.c”, “GUIConf.h”, “LCDConf.h” and “LCDConf.c”.  Copy and paste them into the emWin_config directory.

For this project I am going to use the code that existed in “main.c” from the original PSoC Creator project.  But I want it to be a task (and a few other changes).  To facilitate things, I will copy it as well. Then rename it to eInkTask.c.  And finally, the file “Cypress Logo Full Color_png1bpp.c” needs to be copied as well.

After all of those copies you should have your project looking something like this:

Port the Drivers and eInkTask

Now we need to fix all of the driver code.  Big picture you will need to take the following actions.

  • Update the Project settings to include the new folders (emWin_config and emWin Library)
  • Replace the PSoC Creator #include <project.h> with MTB #include “cycfg.h”
  • Update the files to have #include “FreeRTOS.h” and “task.h” where appropriate
  • Replace all of the CyDelay’s with vTaskDelays
  • Fix the old PSoC Creator component calls for the timer with PDL calls

First go to the project settings (remember, click on the project then select properties).  Then pick “C/C++ Build Settings” then “GNU ARM Cross C Compiler” and “includes”  Press the little green “+” to add the new directories

You can select both directories at once.

Next edit  eInkTask.c

Update #include “project.h” to be #include “cycfg.h” on line 59.  Add “FreeRTOS.h” and “task.h” to the includes.

#include "cycfg.h"
#include "GUI.h"
#include "pervasive_eink_hardware_driver.h"
#include "cy_eink_library.h"
#include "LCDConf.h"
#include "FreeRTOS.h"
#include "task.h"
#include <stdio.h>

Find and replace “CyDelay” with “vTaskDelay”

Update the PSoC Creator component call  _Read with the pdl calls Cy_GPIO_Read on line 661

void WaitforSwitchPressAndRelease(void)
{
/* Wait for SW2 to be pressed */
while(Cy_GPIO_Read(SW2_PORT,SW2_PIN) != 0);
/* Wait for SW2 to be released */
while(Cy_GPIO_Read(SW2_PORT,SW2_PIN) == 0);
}

Update the “int main(void)” to be “void eInkTask(void *arg)” on line 687

void eInkTask(void *arg)
{
(void)arg;

Remove ” __enable_irq(); /* Enable global interrupts. */” from the old main on line 695.

In the file cy_eink_psoc_interface.h

Update the #include <project.h> to be #include “cycfg.h” on line 59.

In the file cy_eink_psoc_interface.c

Create a context for the SPIM by adding on line 58:

cy_stc_scb_spi_context_t CY_EINK_SPIM_context;

The three timer functions in this file use the old PSoC Creator component timer interface APIs rather than the PDL interface.  So you will need to change Cy_EINK_TimerInit, Cy_EINK_GetTimeTick and Cy_EINK_TimerStop to use PDL.

Here is Cy_EINK_TimerInit

void Cy_EINK_TimerInit(void)
{   
/* Clear the counter value and the counter variable */
//CY_EINK_Timer_SetCounter(0);
Cy_TCPWM_Counter_Init (CY_EINK_Timer_HW, CY_EINK_Timer_NUM, &CY_EINK_Timer_config);
Cy_TCPWM_Counter_SetCounter	(	CY_EINK_Timer_HW, CY_EINK_Timer_NUM,0);
Cy_TCPWM_Enable_Multiple(	CY_EINK_Timer_HW,CY_EINK_Timer_MASK);
/* Initialize the Timer */
//CY_EINK_Timer_Start();
Cy_TCPWM_TriggerStart	(	CY_EINK_Timer_HW,CY_EINK_Timer_MASK);
}

And Cy_EINK_GetTimeTick

uint32_t Cy_EINK_GetTimeTick(void)
{
/* Variable used to store the time tick */
uint32_t timingCount;
/* Read the current time tick from the E-INK Timer */
//timingCount = CY_EINK_Timer_GetCounter();
timingCount = Cy_TCPWM_Counter_GetCounter	(CY_EINK_Timer_HW, CY_EINK_Timer_NUM);
/* Return the current value of time tick */
return(timingCount);
}

And Cy_EINK_TimerStop

void Cy_EINK_TimerStop(void)
{
/* Stop the E-INK Timer */
//CY_EINK_Timer_Disable();
Cy_TCPWM_Counter_Disable(CY_EINK_Timer_HW, CY_EINK_Timer_NUM);
}

In  the file LCDConf.h change the include to stdint.h and make the type uint8_t instead of uint8

#include  <stdint.h>
void LCD_CopyDisplayBuffer(uint8_t * destination, int count);

In the file LCDConf.c remove the #include “syslib/cy_syslib.h” (I have no idea why it is/was there) and then add “#include <stdint.h>”  On line 219 change “uint8” to be “uint8_t”

void LCD_CopyDisplayBuffer(uint8_t * destination, int count)

In the file cy_eink_fonts.h change the “#include <project.h>” to be

#include <stdint.h>
#include <stdbool.h>

In main.c add an external reference to the eInkTask on line 36 (yes this is really ugly Alan)

extern void eInkTask(void *);

And start the eInkTask on line 58.  Notice that I put in 10K for the stacksize… but I dont actually know how much it takes.

  	xTaskCreate( eInkTask,"eInkTask", 1024*10,  0,  1, 0  );

Program & Test the MTB Project

When you program the development kit you should have

  1. A blinking RED LED
  2. The ability to scroll through a bunch of screens using the SW2 button.

Here is a picture

In the next article I will:

  1. Speed up the SPI
  2. Get rid of the hardware timer
  3. Explain more about the EINK.

 

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.