Skip to main content

TinyGo Embedded Workshop - Assignment 3: Display

·6 mins·
WS002EN - This article is part of a series.
Part 4: This Article

Assignment 3: Display
#

In this assignment, you’ll learn to drive an LCD display, draw graphics, display text, and show images. The M5Stack Core2 features a 2.0" ILI9342C TFT display (320x240 pixels).

M5Stack Core2 Display Overview
#

Display Specifications
#

  • Controller: ILI9342C
  • Resolution: 320 x 240 pixels
  • Interface: SPI (Serial Peripheral Interface)
  • Color: 16-bit RGB565 (65,536 colors)
  • Backlight: Controlled by AXP192 PMIC

Pin Connections
#

The display connects via SPI:

  • SCK: GPIO18 (Serial Clock)
  • MOSI/SDO: GPIO23 (Master Out Slave In)
  • MISO/SDI: GPIO38 (Master In Slave Out)
  • CS (SS): GPIO5 (Chip Select)
  • DC: GPIO15 (Data/Command)
  • RST: Controlled by AXP192

Power Management
#

The AXP192 power management IC controls:

  • LCD voltage (3.3V)
  • Backlight enable
  • LCD reset sequence

Display Initialization
#

Step 1: Create Project
#

mkdir display-demo
cd display-demo
go mod init display-demo

Step 2: Initialize Display
#

Create main.go:

package main

import (
    "image/color"
    "machine"
    "time"

    "tinygo.org/x/drivers/axp192/m5stack-core2-axp192"
    "tinygo.org/x/drivers/i2csoft"
    "tinygo.org/x/drivers/ili9341"
)

func main() {
    // Initialize serial for debug output
    serial := machine.Serial
    serial.Configure(machine.UARTConfig{BaudRate: 115200})
    serial.WriteString("Initializing display...\r\n")

    // Initialize I2C for AXP192 power management
    i2c := i2csoft.New(machine.SCL0_PIN, machine.SDA0_PIN)
    i2c.Configure(i2csoft.I2CConfig{Frequency: 100e3})

    // Initialize AXP192 PMIC (powers display and backlight)
    axp := axp192.New(i2c)
    axp.Begin()
    axp.SetLCDVoltage(3300)  // 3.3V for LCD
    axp.SetLDO2Voltage(3300)  // LDO2 for peripherals
    axp.SetDCDC3(3300)        // DCDC3 for LCD backlight
    axp.EnableLCD(true)       // Enable LCD power
    axp.EnableBacklight(true) // Enable backlight

    serial.WriteString("AXP192 initialized\r\n")

    // Initialize SPI for display
    machine.SPI2.Configure(machine.SPIConfig{
        SCK:       machine.LCD_SCK_PIN,
        SDO:       machine.LCD_SDO_PIN,
        SDI:       machine.LCD_SDI_PIN,
        Frequency: 40e6, // 40MHz
    })

    serial.WriteString("SPI initialized\r\n")

    // Initialize ILI9342C display
    display := ili9341.NewSPI(
        machine.SPI2,
        machine.LCD_DC_PIN,
        machine.LCD_SS_PIN,
        machine.NoPin,
    )

    display.Configure(ili9341.Config{
        Width:            320,
        Height:           240,
        DisplayInversion: true,
    })

    display.SetRotation(ili9341.Rotation0Mirror)

    serial.WriteString("Display initialized!\r\n")

    // Clear screen with blue background
    display.FillScreen(color.RGBA{20, 20, 60, 255})

    // Keep display on
    for {
        time.Sleep(time.Second)
    }
}

Step 3: Build and Flash
#

tinygo flash -target m5stack-core2 -port /dev/ttyUSB0 .

The screen should turn blue with a slight purple tint.

Drawing Shapes
#

Fill Screen
#

// Fill entire screen with a color
display.FillScreen(color.RGBA{20, 20, 60, 255})

Draw Pixels
#

// Draw a single pixel at (x, y)
display.SetPixel(160, 120, color.RGBA{255, 0, 0, 255})

Draw Lines
#

// Draw line from (x1, y1) to (x2, y2)
display.DrawLine(10, 10, 310, 10, color.RGBA{255, 0, 0, 255})    // Red top line
display.DrawLine(310, 10, 310, 230, color.RGBA{0, 255, 0, 255})  // Green right line
display.DrawLine(310, 230, 10, 230, color.RGBA{0, 0, 255, 255})  // Blue bottom line
display.DrawLine(10, 230, 10, 10, color.RGBA{255, 255, 0, 255})  // Yellow left line

Draw Rectangles
#

// Draw filled rectangle
display.FillRectangle(50, 50, 100, 80, color.RGBA{255, 0, 0, 255})

// Draw rectangle outline
drawRectangle(50, 150, 100, 80, color.RGBA{0, 255, 0, 255})

Draw Circles
#

// Draw filled circle
display.FillCircle(160, 120, 40, color.RGBA{0, 0, 255, 255})

// Draw circle outline
display.DrawCircle(160, 120, 50, color.RGBA{255, 255, 0, 255})

Draw Rounded Rectangles
#

// Draw rounded rectangle
display.DrawRoundRect(20, 20, 280, 200, 20, color.RGBA{255, 255, 255, 255})

Draw Triangles
#

// Draw filled triangle
display.FillTriangle(160, 20, 20, 220, 300, 220, color.RGBA{255, 0, 255, 255})

Complete Graphics Demo
#

package main

import (
    "image/color"
    "machine"
    "time"

    "tinygo.org/x/drivers/axp192/m5stack-core2-axp192"
    "tinygo.org/x/drivers/i2csoft"
    "tinygo.org/x/drivers/ili9341"
)

func main() {
    // Initialize display (as shown above)
    // ... initialization code ...

    display.FillScreen(color.RGBA{20, 20, 60, 255})

    // Draw shapes
    display.DrawLine(10, 10, 310, 10, color.RGBA{255, 0, 0, 255})
    display.FillRectangle(50, 50, 100, 80, color.RGBA{255, 0, 0, 255})
    display.FillCircle(160, 120, 40, color.RGBA{0, 0, 255, 255})
    display.DrawCircle(160, 120, 50, color.RGBA{255, 255, 0, 255})

    for {
        time.Sleep(time.Second)
    }
}

Displaying Text
#

Using Built-in Font
#

The ILI9341 driver includes a basic 8x8 pixel font:

// Draw text at (x, y) with color and background
display.DrawRGBBitmap(10, 10, []byte("Hello TinyGo!"), color.RGBA{255, 255, 255, 255})

Using tinygl-font
#

For better text rendering, use the tinygl-font library:

package main

import (
    "image/color"
    "machine"
    "time"

    "tinygo.org/x/drivers/axp192/m5stack-core2-axp192"
    "tinygo.org/x/drivers/i2csoft"
    "tinygo.org/x/drivers/ili9341"
    "tinygo.org/x/drivers/pixel"
    "tinygo.org/x/tinygl-font"
    "tinygo.org/x/tinygl-font/roboto"
)

func main() {
    // Initialize display
    // ... initialization code ...

    // Create off-screen buffer for text
    textDisplay := pixel.NewImage[pixel.RGB565BE](300, 60)

    // Colors
    white := pixel.NewRGB565BE(color.RGBA{255, 255, 255, 255})
    bgColor := pixel.NewRGB565BE(color.RGBA{20, 20, 60, 255})

    // Clear buffer with background color
    textDisplay.FillSolidColor(bgColor)

    // Draw text using Roboto 48pt font
    font.Draw(roboto.Regular48, "Hello!", 0, 48, white, textDisplay)

    // Get pixel data and display
    pixelData := textDisplay.RawBuffer()
    width, height := textDisplay.Size()
    display.DrawRGBBitmap8(10, 90, pixelData, int16(width), int16(height))

    for {
        time.Sleep(time.Second)
    }
}

Displaying Images
#

Embedded Image Example
#

Embed a PNG image as a Go constant:

package main

import (
    "image/color"
    "machine"
    "strings"
    "time"

    "tinygo.org/x/drivers/axp192/m5stack-core2-axp192"
    "tinygo.org/x/drivers/i2csoft"
    "tinygo.org/x/drivers/ili9341"
    "tinygo.org/x/drivers/image/png"
)

//go:generate go run ../cmd/embed-png/main.go logo.png > logo.go

func main() {
    // Initialize display
    // ... initialization code ...

    // Decode PNG image
    var logoBuffer [3 * 8 * 8 * 4]uint16
    logoReader := strings.NewReader(tinygoLogoPNG)

    png.SetCallback(logoBuffer[:], func(data []uint16, x, y, w, h, width, height int16) {
        offsetX := int16((320 - 299) / 2) // Center horizontally
        offsetY := int16(0)
        display.DrawRGBBitmap(x+offsetX, y+offsetY, data[:w*h], w, h)
    })

    png.Decode(logoReader)

    for {
        time.Sleep(time.Second)
    }
}

Color Models
#

RGB565 Color Format
#

The display uses 16-bit RGB565:

  • Red: 5 bits (32 shades)
  • Green: 6 bits (64 shades)
  • Blue: 5 bits (32 shades)

Creating colors:

// From RGBA (converted to RGB565)
color.RGBA{255, 0, 0, 255}       // Red
color.RGBA{0, 255, 0, 255}       // Green
color.RGBA{0, 0, 255, 255}       // Blue
color.RGBA{255, 255, 255, 255}   // White
color.RGBA{0, 0, 0, 255}         // Black

Common Colors
#

var (
    Red     = color.RGBA{255, 0, 0, 255}
    Green   = color.RGBA{0, 255, 0, 255}
    Blue    = color.RGBA{0, 0, 255, 255}
    Yellow  = color.RGBA{255, 255, 0, 255}
    Cyan    = color.RGBA{0, 255, 255, 255}
    Magenta = color.RGBA{255, 0, 255, 255}
    White   = color.RGBA{255, 255, 255, 255}
    Black   = color.RGBA{0, 0, 0, 255}
)

Display Rotation
#

The display supports 4 rotation modes:

display.SetRotation(ili9341.Rotation0)        // Normal
display.SetRotation(ili9341.Rotation0Mirror)  // Normal mirrored
display.SetRotation(ili9341.Rotation90)       // 90 degrees clockwise
display.SetRotation(ili9341.Rotation180)      // 180 degrees
display.SetRotation(ili9341.Rotation270)      // 270 degrees clockwise

Animation Example
#

Create simple animation:

var ballX int16 = 160
var ballY int16 = 120
var velX int16 = 2
var velY int16 = 3

func main() {
    // Initialize display
    // ... initialization code ...

    for {
        // Clear screen
        display.FillScreen(color.RGBA{20, 20, 60, 255})

        // Update position
        ballX += velX
        ballY += velY

        // Bounce off walls
        if ballX <= 10 || ballX >= 310 {
            velX = -velX
        }
        if ballY <= 10 || ballY >= 230 {
            velY = -velY
        }

        // Draw ball
        display.FillCircle(ballX, ballY, 10, color.RGBA{255, 255, 0, 255})

        time.Sleep(time.Millisecond * 33) // ~30 FPS
    }
}

Troubleshooting
#

Display is blank/flickering
#

  • Check AXP192 initialization (powers display)
  • Verify SPI connections
  • Check backlight is enabled
  • Try resetting the board

Wrong colors
#

  • Ensure RGB565 format (not RGBA)
  • Check byte order (RGB565BE vs RGB565LE)
  • Verify color values are correct

Text not visible
#

  • Check Y coordinate allows room for font height
  • Ensure text color contrasts with background
  • Verify font is loaded correctly

Compilation errors
#

  • Ensure all imports are correct
  • Check go.mod has required dependencies
  • Verify TinyGo 0.41+ installed

Summary
#

In this assignment, you learned:

  • How to initialize an LCD display via SPI
  • Power management with AXP192
  • Drawing shapes: lines, rectangles, circles
  • Displaying text with fonts
  • Showing images from embedded data
  • Understanding RGB565 color format
  • Creating simple animations

You can now create visual interfaces for your projects!

Assignment 4: Sensors

WS002EN - This article is part of a series.
Part 4: This Article

Related