Working With Graphics On A St7789 Display Using Meadow
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: Microsoft, Meadow, Wilderness Labs
Estimated time: 1 hour
License: Apache License 2.0 (Apache-2.0)
Items used in this project
Hardware components
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 circuitFor this project, we're going to use a ST7789
TFT 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
Step 2 - Create a Meadow Application ProjectCreate 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 packageWindows
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
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
Step 3.1 - Displaying basic shapesIn 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
Step 3.2 - Displaying textNow 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
Step 3.3 - Displaying a watch faceNow 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
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.
Leave your feedback...