Working With Graphics On A St7789 Display Using Meadow

Photo of jorgedevs

Made by jorgedevs

About the project

Learn how to connect an SPI LCD display to your Meadow board to draw shapes, text and images with the Meadow. Foundation Graphics Library.

Project info

Difficulty: Easy

Platforms: MicrosoftMeadowWilderness Labs

Estimated time: 1 hour

License: Apache License 2.0 (Apache-2.0)

Items used in this project

Hardware components

Standard LCD 16x2 white on blue Standard LCD 16x2 white on blue x 1
Tiny Breadboard Tiny Breadboard x 1
Tiny Breadboard Tiny Breadboard x 1
Raspberry Pi 4 Model B 1GB, 2GB, 4GB or 8GB RAM Raspberry Pi 4 Model B 1GB, 2GB, 4GB or 8GB RAM x 1

Software apps and online services

Microsoft Visual Studio 2019 Microsoft Visual Studio 2019

Story

The purpose of this project is to get familiar with Meadow.Foundation's GraphicsLibrary, a very intuitive and powerful API that can draw shapes, lines, text and images with just a few lines of code.

We will connect an SPI TFT LCD display and go through three parts to get familiar with the display graphics library: 1. Drawing basic shapes and lines, 2. Drawing texts with different font sizes, and 3. An example with a combination of both to create an analog watch face.

Meadow.Foundation is a platform for quickly and easily building connected things using.NET on Meadow. Created by Wilderness Labs, it's completely open source and maintained by the Wilderness Labs community.

If you're new working with Meadow, I suggest you go to the Getting Started w/ Meadow by Controlling the Onboard RGB LED project to properly set up your development environment.

Step 1 - Assemble the circuit

For this project, we're going to use a ST7789TFT SPI display. Connect the display to Meadow as shown in the Fritzing diagram:

Circuit diagram to connect a Meadow with a SPI display

Circuit diagram to connect a Meadow with a SPI display

Circuit diagram to connect a Meadow with a SPI display

Step 2 - Create a Meadow Application Project

Create a new Meadow Application project in Visual Studio 2019 for Windows or macOS; since our game is a variation of the game Simon, let's call it MeadowClockGraphics.

Step 3 - Add the Display's NuGet package

Windows

Right-click on your MeadowClockGraphics project and click Manage NuGet Packages. In the Browse tab, search for Meadow.Foundation.Displays.TftSpi and click Install to add it to your project.

Adding Meadow.Foundation.Displays.TftSpi NuGet package

Adding Meadow.Foundation.Displays.TftSpi NuGet package

Adding Meadow.Foundation.Displays.TftSpi NuGet package

macOS

Alt-click on your MeadowClockGraphics project in the Solution Explorer, and click Add => Add Nuget Package to open the NuGet Package window. Search for the Meadow.Foundation.Displays.TftSpi and click Install to add it to your project.

Adding Meadow.Foundation.Displays.TftSpi NuGet package

Adding Meadow.Foundation.Displays.TftSpi NuGet package

Adding Meadow.Foundation.Displays.TftSpi NuGet package

Step 3.1 - Displaying basic shapes

In this first section, we'll learn how to display basic shapes and lines. In your MeadowApp class, add paste the following code:

using System.Threading;using Meadow;using Meadow.Devices;using Meadow.Foundation;using Meadow.Foundation.Displays.Tft;using Meadow.Foundation.Graphics;namespace MeadowClockGraphics{    public class MeadowApp : App<F7Micro, MeadowApp>    {        St7789 st7789;        GraphicsLibrary graphics;        public MeadowApp()        {            var config = new SpiClockConfiguration(6000,                 SpiClockConfiguration.Mode.Mode3);            st7789 = new St7789            (                device: Device,                spiBus: Device.CreateSpiBus(                    Device.Pins.SCK, Device.Pins.MOSI, Device.Pins.MISO, config),                chipSelectPin: null,                dcPin: Device.Pins.D01,                resetPin: Device.Pins.D00,                width: 240, height: 240            );            graphics = new GraphicsLibrary(st7789);            graphics.Rotation = GraphicsLibrary.RotationType._270Degrees;            DrawShapes();        }        void DrawShapes()        {            Random rand = new Random();            graphics.Clear(true);            int radius = 10;            int originX = displayWidth / 2;            int originY = displayHeight / 2;            for (int i=1; i<5; i++)            {                graphics.DrawCircle                (                    centerX: originX,                    centerY: originY,                    radius: radius,                    color: Color.FromRgb(                        rand.Next(255), rand.Next(255), rand.Next(255))                );                graphics.Show();                radius += 30;            }            int sideLength = 30;            for (int i = 1; i < 5; i++)            {                graphics.DrawRectangle                (                    xLeft: (displayWidth - sideLength) / 2,                    yTop: (displayHeight - sideLength) / 2,                    width: sideLength,                    height: sideLength,                    color: Color.FromRgb(                        rand.Next(255), rand.Next(255), rand.Next(255))                );                graphics.Show();                sideLength += 60;            }            graphics.DrawLine(0, displayHeight / 2, displayWidth, displayHeight / 2,                Color.FromRgb(rand.Next(255), rand.Next(255), rand.Next(255)));            graphics.DrawLine(displayWidth / 2, 0, displayWidth / 2, displayHeight,                Color.FromRgb(rand.Next(255), rand.Next(255), rand.Next(255)));            graphics.DrawLine(0, 0, displayWidth, displayHeight,                Color.FromRgb(rand.Next(255), rand.Next(255), rand.Next(255)));            graphics.DrawLine(0, displayHeight, displayWidth, 0,                Color.FromRgb(rand.Next(255), rand.Next(255), rand.Next(255)));            graphics.Show();            Thread.Sleep(5000);        }    }}

First thing that happens in this class is initialize the ST7789 display connected to Meadow and when creating a GraphicLibrary object, note that we pass the display object. This links the graphics library to the display we're using.

After Initializing everything, at the end of the constructor it calls the method DrawShapes, where you can see all the API methods that graphics offers to draw shapes and lines, specifying colors, filled or empty, sizes, etc. It's important to note that when you want to clear the screen you call graphics.Clear(true); and when you want to display everything you have in the buffer you call graphics.Show();

Note: Make sure when drawing shapes you don't exceed the screen size, otherwise you'll get an OutOfBoundsException.

Run the project

Click the Run button in Visual Studio to see your shapes in the LCD display! Feel free to play around with more colors and sizes to have a It should look like to the following gif:

Displaying geometric shapes

Displaying geometric shapes

Displaying geometric shapes

Step 3.2 - Displaying text

Now in the following code example you'll see how to display text of different font sizes. In your MeadowApp class, add the following code or replace it altogether:

using System.Threading;using Meadow;using Meadow.Devices;using Meadow.Foundation;using Meadow.Foundation.Displays.Tft;using Meadow.Foundation.Graphics;namespace MeadowClockGraphics{    public class MeadowApp : App<F7Micro, MeadowApp>    {        St7789 st7789;        GraphicsLibrary graphics;        public MeadowApp()        {            var config = new SpiClockConfiguration(6000,                 SpiClockConfiguration.Mode.Mode3);            st7789 = new St7789            (                device: Device,                spiBus: Device.CreateSpiBus(                    Device.Pins.SCK, Device.Pins.MOSI, Device.Pins.MISO, config),                chipSelectPin: Device.Pins.D02,                dcPin: Device.Pins.D01,                resetPin: Device.Pins.D00,                width: 240, height: 240            );            graphics = new GraphicsLibrary(st7789);            graphics.Rotation = GraphicsLibrary.RotationType._270Degrees;            DrawTexts();        }        void DrawTexts()        {            graphics.Clear(true);            int indent = 20;            int spacing = 20;            int y = 5;            graphics.CurrentFont = new Font12x16();            graphics.DrawText(indent, y, "Meadow F7 SPI ST7789!!");            graphics.DrawText(indent, y += spacing, "Red", Color.Red);            graphics.DrawText(indent, y += spacing, "Purple", Color.Purple);            graphics.DrawText(indent, y += spacing, "BlueViolet", Color.BlueViolet);            graphics.DrawText(indent, y += spacing, "Blue", Color.Blue);            graphics.DrawText(indent, y += spacing, "Cyan", Color.Cyan);            graphics.DrawText(indent, y += spacing, "LawnGreen", Color.LawnGreen);            graphics.DrawText(indent, y += spacing, "GreenYellow", Color.GreenYellow);            graphics.DrawText(indent, y += spacing, "Yellow", Color.Yellow);            graphics.DrawText(indent, y += spacing, "Orange", Color.Orange);            graphics.DrawText(indent, y += spacing, "Brown", Color.Brown);            graphics.Show();            Thread.Sleep(5000);        }    }}

Inside DrawTexts, notice that before calling the DrawText method, we specify the font size in the CurrentFont property of graphics. Next we have a series of DrawText calls, where you have to specify the x and y coordinates along with the text string and specify a Color if the display supports it.

Note: The current version of DisplayGraphics doesn't support text wrapping, so make sure the text you want to display fits in your screen, otherwise you'll get an OutOfBoundsException.

Run the project

Click the Run button in Visual Studio to see your text samples in the display! Notice how it can also display certain especial characters. It should look like to the following gif:

Displaying text with different colors

Displaying text with different colors

Displaying text with different colors

Step 3.3 - Displaying a watch face

Now lets combine what we've learned so far and make a watch face as an example. Replace the entire MeadowApp class code with the following, and we'll go over the logic below:

using System.Threading;using Meadow;using Meadow.Devices;using Meadow.Foundation;using Meadow.Foundation.Displays.Tft;using Meadow.Foundation.Graphics;namespace DisplayGraphicsSPI{    public class MeadowApp : App<F7Micro, MeadowApp>    {        readonly Color WatchBackgroundColor = Color.White;        St7789 st7789;        GraphicsLibrary graphics;        int displayWidth, displayHeight;        int hour, minute, second, tick;        public MeadowApp()        {            var config = new SpiClockConfiguration(6000,                 SpiClockConfiguration.Mode.Mode3);            st7789 = new St7789            (                device: Device,                spiBus: Device.CreateSpiBus(                    Device.Pins.SCK, Device.Pins.MOSI, Device.Pins.MISO, config),                chipSelectPin: Device.Pins.D02,                dcPin: Device.Pins.D01,                resetPin: Device.Pins.D00,                width: 240, height: 240            );            graphics = new GraphicsLibrary(st7789);            graphics.Rotation = GraphicsLibrary.RotationType._270Degrees;            DrawClock();        }        void DrawClock()        {            graphics.Clear(true);            hour = 8;            minute = 54;            DrawWatchFace();            while (true)            {                tick++;                Thread.Sleep(1000);                UpdateClock(second: tick % 60);            }        }        void DrawWatchFace()        {            graphics.Clear();            int hour = 12;            int xCenter = displayWidth / 2;            int yCenter = displayHeight / 2;            int x, y;            graphics.DrawRectangle(0, 0, displayWidth, displayHeight, Color.White);            graphics.DrawRectangle(5, 5, displayWidth - 10, displayHeight - 10, Color.White);            graphics.CurrentFont = new Font12x20();            graphics.DrawCircle(xCenter, yCenter, 100, WatchBackgroundColor, true);            for (int i = 0; i < 60; i++)            {                x = (int)(xCenter + 80 * Math.Sin(i * Math.PI / 30));                y = (int)(yCenter - 80 * Math.Cos(i * Math.PI / 30));                if (i % 5 == 0)                {                    graphics.DrawText(                        hour > 9? x-10 : x-5, y-5, hour.ToString(), Color.Black);                    if (hour == 12) hour = 1; else hour++;                }            }            graphics.Show();        }        void UpdateClock(int second = 0)        {            int xCenter = displayWidth / 2;            int yCenter = displayHeight / 2;            int x, y, xT, yT;            if (second == 0)            {                minute++;                if (minute == 60)                {                    minute = 0;                    hour++;                    if (hour == 12)                    {                        hour = 0;                    }                }            }            //remove previous hour            int previousHour = (hour - 1) < -1 ? 11 : (hour - 1);            x = (int)(xCenter + 43 * System.Math.Sin(previousHour * System.Math.PI / 6));            y = (int)(yCenter - 43 * System.Math.Cos(previousHour * System.Math.PI / 6));            xT = (int)(xCenter + 3 * System.Math.Sin((previousHour - 3) * System.Math.PI / 6));            yT = (int)(yCenter - 3 * System.Math.Cos((previousHour - 3) * System.Math.PI / 6));            graphics.DrawLine(xT, yT, x, y, WatchBackgroundColor);            xT = (int)(xCenter + 3 * System.Math.Sin((previousHour + 3) * System.Math.PI / 6));            yT = (int)(yCenter - 3 * System.Math.Cos((previousHour + 3) * System.Math.PI / 6));            graphics.DrawLine(xT, yT, x, y, WatchBackgroundColor);            //current hour            x = (int)(xCenter + 43 * System.Math.Sin(hour * System.Math.PI / 6));            y = (int)(yCenter - 43 * System.Math.Cos(hour * System.Math.PI / 6));            xT = (int)(xCenter + 3 * System.Math.Sin((hour - 3) * System.Math.PI / 6));            yT = (int)(yCenter - 3 * System.Math.Cos((hour - 3) * System.Math.PI / 6));graphics.DrawLine(xT, yT, x, y, Color.Black);            xT = (int)(xCenter + 3 * System.Math.Sin((hour + 3) * System.Math.PI / 6));            yT = (int)(yCenter - 3 * System.Math.Cos((hour + 3) * System.Math.PI / 6));            graphics.DrawLine(xT, yT, x, y, Color.Black);            //remove previous minute            int previousMinute = minute - 1 < -1 ? 59 : (minute - 1);            x = (int)(xCenter + 55 * System.Math.Sin(previousMinute * System.Math.PI / 30));            y = (int)(yCenter - 55 * System.Math.Cos(previousMinute * System.Math.PI / 30));            xT = (int)(xCenter + 3 * System.Math.Sin((previousMinute - 15) * System.Math.PI / 6));            yT = (int)(yCenter - 3 * System.Math.Cos((previousMinute - 15) * System.Math.PI / 6));            graphics.DrawLine(xT, yT, x, y, WatchBackgroundColor);            xT = (int)(xCenter + 3 * System.Math.Sin((previousMinute + 15) * System.Math.PI / 6));            yT = (int)(yCenter - 3 * System.Math.Cos((previousMinute + 15) * System.Math.PI / 6));            graphics.DrawLine(xT, yT, x, y, WatchBackgroundColor);            //current minute            x = (int)(xCenter + 55 * System.Math.Sin(minute * System.Math.PI / 30));            y = (int)(yCenter - 55 * System.Math.Cos(minute * System.Math.PI / 30));            xT = (int)(xCenter + 3 * System.Math.Sin((minute - 15) * System.Math.PI / 6));            yT = (int)(yCenter - 3 * System.Math.Cos((minute - 15) * System.Math.PI / 6));            graphics.DrawLine(xT, yT, x, y, Color.Black);            xT = (int)(xCenter + 3 * System.Math.Sin((minute + 15) * System.Math.PI / 6));            yT = (int)(yCenter - 3 * System.Math.Cos((minute + 15) * System.Math.PI / 6));            graphics.DrawLine(xT, yT, x, y, Color.Black);            //remove previous second            int previousSecond = second - 1 < -1 ? 59 : (second - 1);            x = (int)(xCenter + 70 * System.Math.Sin(previousSecond * System.Math.PI / 30));            y = (int)(yCenter - 70 * System.Math.Cos(previousSecond * System.Math.PI / 30));            graphics.DrawLine(xCenter, yCenter, x, y, WatchBackgroundColor);            //current second            x = (int)(xCenter + 70 * System.Math.Sin(second * System.Math.PI / 30));            y = (int)(yCenter - 70 * System.Math.Cos(second * System.Math.PI / 30));            graphics.DrawLine(xCenter, yCenter, x, y, Color.Red);            graphics.Show();        }    }}

Lets break down the explanation, starting from the DrawClock method that's invoked at the end of MeadowApp's constructor:

  • DrawClock is in charge of initialize the initial hour and minutes of the time, calls DrawWatchFace, and enters into a infinite while loop that in every 1000ms (or 1s) calls UpdateClock passing the remainder operator of ticks divided by 60.
  • DrawWatchFace method is called once, and it draws the watch's background, hour numbers.
  • UpdateClock displays the clock's hour, minute and second hand, and its invoked every second to update the time like any regular clock. Notice that before drawing the new arm's position, we are re-drawing the arms current position with the same color as the background. We do this to update all the hands positions without having to clear and redraw the entire display in each iteration.

Run the project

Click the Run button in Visual Studio to see your clock in action! It should look like to the following gif:

Displaying watch face

Displaying watch face

Displaying watch face

Note that the time on the clock will be displayed based on the initial hour and minute values set in the DrawClock method. You could wire an RTC module to keep the time regardless if Meadow loses power, or use push buttons to adjust the time anytime you need to.

Check out Meadow.Foundation!

This project is only the tip of the iceberg in terms of the extensive exciting things you can do with Meadow.Foundation.

  • It comes with a huge peripheral driver library with drivers for the most common sensors and peripherals.
  • The peripheral drivers encapsulate the core logic and expose a simple, clean, modern API.
  • This project is backed by a growing community that is constantly working on building cool connected things and are always excited to help new-comers and discuss new projects.

References

Schematics, diagrams and documents

Diagram Circuit

How to connect Meadow with ILI9163 display

Code

Complete project

Look for the complete project sample in Meadow.Samples/Source/MeadowSamples/Projects/MeadowClockGraphics/

Credits

Leave your feedback...