The TFT FeatherWing - 2.4” 320x240 Touchscreen is an unusal wing in that it comes fully assembled and provides a socket in which to place the Feather M4 Express. This makes all the necessary connections to allow the Feather to drive the display.

That connection is, specifically, a Serial Peripheral Interface (SPI) bus connection to the ILI9341 TFT LCD Single Chip Driver.

Luckily for me, someone has already written an embedded graphics driver for the ILI9341. Let’s use it to create one of the classic graphics “Hello World” programs; the bouncing box.

#![no_std]
#![no_main]

use feather_m4 as bsp;

use bsp::{
    entry,
    hal::{clock::GenericClockController, delay::Delay, prelude::*},
    pac::{CorePeripherals, Peripherals},
};
use display_interface_spi::SPIInterface;
use embedded_graphics::{
    pixelcolor::Rgb565,
    prelude::*,
    primitives::{PrimitiveStyleBuilder, Rectangle, RoundedRectangle, StyledDimensions},
};
use ili9341::{DisplaySize240x320, Ili9341, Orientation};
use panic_semihosting as _;
use rtt_target::{rprintln, rtt_init_print};

pub const SCREEN_SIZE: Size = Size::new(320, 240);

#[entry]
fn main() -> ! {
    rtt_init_print!();
    rprintln!("bouncing box");
    let mut peripherals = Peripherals::take().unwrap();
    let core = CorePeripherals::take().unwrap();
    let mut clocks = GenericClockController::with_external_32kosc(
        peripherals.GCLK,
        &mut peripherals.MCLK,
        &mut peripherals.OSC32KCTRL,
        &mut peripherals.OSCCTRL,
        &mut peripherals.NVMCTRL,
    );
    let pins = bsp::Pins::new(peripherals.PORT);

    let sck = pins.sck;
    let miso = pins.miso;
    let mosi = pins.mosi;
    let mclk = &mut peripherals.MCLK;

    let lcd_dc = pins.d10.into_push_pull_output();
    let lcd_cs = pins.d9.into_push_pull_output();

    let sercom = peripherals.SERCOM1;
    let spi = bsp::spi_master(&mut clocks, 32.mhz(), sercom, mclk, sck, mosi, miso);
    let spi_iface = SPIInterface::new(spi, lcd_dc, lcd_cs);
    let reset_pin = pins.d12.into_push_pull_output();

    let mut delayer = Delay::new(core.SYST, &mut clocks);

    let mut lcd = Ili9341::new(
        spi_iface,
        reset_pin,
        &mut delayer,
        Orientation::Landscape,
        DisplaySize240x320,
    )
    .unwrap();

    lcd.clear(Rgb565::BLACK).expect("clear");

    let style = PrimitiveStyleBuilder::new()
        .fill_color(Rgb565::GREEN)
        .build();

    let bounds = Rectangle::new(Point::new(5, 5), Size::new(40, 40));
    let mut step = Point::new(1, 1);

    let mut rr = RoundedRectangle::with_equal_corners(bounds, Size::new(10, 10)).into_styled(style);

    rr.draw(&mut lcd).expect("RoundedRectangle");

    loop {
        let old_styled_bounds = rr.primitive.styled_bounding_box(&style);
        rr.translate_mut(step);
        let styled_bounds = rr.primitive.styled_bounding_box(&style);
        lcd.fill_solid(&styled_bounds, Rgb565::BLACK)
            .expect("fill_solid");
        rr.draw(&mut lcd).expect("RoundedRectangle");

        let bottom_right = styled_bounds.bottom_right().expect("bottom_right");

        if styled_bounds.top_left.x <= 0 {
            step.x = 1;
        } else if bottom_right.x > 320 {
            step.x = -1;
        }

        if styled_bounds.top_left.y <= 0 {
            step.y = 1;
        } else if bottom_right.y > 240 {
            step.y = -1;
        }
    }
}

Here’s what it look like.

There’s two problems here; the first is that is pretty slow. The box is only 40 by 40 pixels at two bytes per pixel for 3,200 bytes of data. The way I written this, it has to write that twice, once to erase and once to draw in the new position. That works out to 52,000 bits. Make it 60,000 to account for bus overhead.

If the SPI bus is clocking at 32 Mhz, as I’ve set it, it should be able to move the square 500 times a second if the limit is SPI bus speed. It doesn’t appear to be anywhere close to that.

The data sheet says the maximum supported SPI frequency is 10mhz, so perhaps that’s it.

The second problem is the rippling of the square, called tearing.

Screen tearing is a visual artifact in video display where a display device shows information from multiple frames in a single screen draw.

The Wikipedia article describes a number of techniques to overcome tearing, but none of them I can easily implement. The hardware signal from the ILI9341 that one could use to better time updates isn’t brought out on the Adafruit board. There’s also a register on the board to access the current scan line, which one could use to find a better time to update, but the driver doesn’t support reading from the ILI9341, only writing to it. I think I’m going to have to grin and bear it.

Luckily, Quinti-Maze doesn’t modify much of the screen at a time, so I think this tearing will not be visible. I will also make some effort not to erase and redraw pixels that don’t change from frame to frame, which should help.