/*
* ============================================================================
* Bare-Metal ILI9341 Color TFT Demo
* Target: ATmega328P @ 16 MHz (Arduino Uno R3)
* ============================================================================
*
* WHAT IT DOES
* ------------
* Drives a 240x320 ILI9341 color TFT display over hardware SPI to render:
* - A blue title bar with "BARE METAL SPI" in scaled white text
* - Seven horizontal color stripes (red, orange, yellow, green,
* cyan, blue, magenta) demonstrating RGB565 color
* - Status labels at the bottom of the screen
* - A small white square that bounces back and forth across the
* title bar in an infinite loop
*
* No Arduino libraries are used. Every register write, SPI transfer,
* and display command is handled directly. The Adafruit_GFX,
* TFT_eSPI, and SPI libraries are deliberately avoided in favor of
* understanding the hardware end-to-end.
*
* HOW IT WORKS
* ------------
* Hardware SPI (SPCR/SPDR/SPSR) clocks at F_CPU/2 = 8 MHz, giving
* fast pixel throughput. The ATmega328P only has 2 KB of RAM, so a
* full 150 KB framebuffer is impossible -- pixels are streamed
* directly to the display's internal RAM. To draw a region we set a
* "window" with the column/page address commands (0x2A / 0x2B), then
* pour 16-bit RGB565 pixels into it via Memory Write (0x2C).
*
* A small 5x7 ASCII font (' ' through 'Z', stored in flash via
* PROGMEM) is rendered at runtime by drawing each font pixel as an
* N x N filled rectangle. This gives free integer scaling at the
* cost of chunky pixels.
*
* WIRING (ILI9341 -> Arduino Uno)
* -------------------------------
* VCC -> 5V
* GND -> GND
* CS -> D10 (PB2)
* RESET -> D8 (PB0)
* DC -> D9 (PB1)
* MOSI -> D11 (PB3) [hardware SPI MOSI]
* SCK -> D13 (PB5) [hardware SPI SCK]
* LED -> 5V [backlight always on]
* MISO -> not connected (display is write-only here)
*
* BUILD
* -----
* avr-gcc -mmcu=atmega328p -DF_CPU=16000000UL -Os -o tft.elf tft.c
* avr-objcopy -O ihex tft.elf tft.hex
* avrdude -c arduino -p atmega328p -P <port> -b 115200 \
* -U flash:w:tft.hex
*
* Also runs unmodified in the Wokwi simulator with an Arduino Uno
* and an ILI9341 component. Highly recommended: attach a logic
* analyzer to MOSI/SCK/CS/DC and watch the SPI traffic in PulseView.
*
* ============================================================================
*/
#include <avr/io.h>
#include <avr/pgmspace.h>
#include <util/delay.h>
#include <stdint.h>
// ============================================================
// Pin assignments
// MOSI = PB3 (11), MISO = PB4 (12, unused), SCK = PB5 (13)
// CS = PB2 (10)
// DC = PB1 (9)
// RST = PB0 (8)
// ============================================================
#define CS_LOW() (PORTB &= ~(1 << PB2))
#define CS_HIGH() (PORTB |= (1 << PB2))
#define DC_CMD() (PORTB &= ~(1 << PB1))
#define DC_DATA() (PORTB |= (1 << PB1))
#define RST_LOW() (PORTB &= ~(1 << PB0))
#define RST_HIGH() (PORTB |= (1 << PB0))
// ============================================================
// SPI - hardware, F_CPU/2 = 8 MHz
// ============================================================
void spi_init(void) {
// MOSI, SCK, CS, DC, RST as outputs
DDRB |= (1 << DDB3) | (1 << DDB5) | (1 << DDB2) | (1 << DDB1) | (1 << DDB0);
CS_HIGH();
DC_DATA();
RST_HIGH();
SPCR = (1 << SPE) | (1 << MSTR); // master mode
SPSR = (1 << SPI2X); // 2x speed -> 8 MHz
}
static inline void spi_write(uint8_t data) {
SPDR = data;
while (!(SPSR & (1 << SPIF)));
}
// ============================================================
// ILI9341 driver
// ============================================================
#define TFT_W 240
#define TFT_H 320
void tft_cmd(uint8_t cmd) {
DC_CMD();
CS_LOW();
spi_write(cmd);
CS_HIGH();
}
void tft_data(uint8_t d) {
DC_DATA();
CS_LOW();
spi_write(d);
CS_HIGH();
}
void tft_init(void) {
// Hardware reset
RST_HIGH();
_delay_ms(5);
RST_LOW();
_delay_ms(20);
RST_HIGH();
_delay_ms(150);
tft_cmd(0x01); // software reset
_delay_ms(150);
tft_cmd(0x28); // display off
tft_cmd(0xCF); // Power Control B
tft_data(0x00); tft_data(0x83); tft_data(0x30);
tft_cmd(0xED); // Power On Sequence Control
tft_data(0x64); tft_data(0x03); tft_data(0x12); tft_data(0x81);
tft_cmd(0xE8); // Driver Timing Control A
tft_data(0x85); tft_data(0x01); tft_data(0x79);
tft_cmd(0xCB); // Power Control A
tft_data(0x39); tft_data(0x2C); tft_data(0x00); tft_data(0x34); tft_data(0x02);
tft_cmd(0xF7); // Pump Ratio Control
tft_data(0x20);
tft_cmd(0xEA); // Driver Timing Control B
tft_data(0x00); tft_data(0x00);
tft_cmd(0xC0); tft_data(0x26); // Power Control 1
tft_cmd(0xC1); tft_data(0x11); // Power Control 2
tft_cmd(0xC5); tft_data(0x35); tft_data(0x3E); // VCOM Control 1
tft_cmd(0xC7); tft_data(0xBE); // VCOM Control 2
tft_cmd(0x36); tft_data(0x48); // Memory Access Control (BGR, normal orientation)
tft_cmd(0x3A); tft_data(0x55); // Pixel Format = 16 bits/pixel
tft_cmd(0xB1); tft_data(0x00); tft_data(0x1B); // Frame Rate
tft_cmd(0xB7); tft_data(0x07); // Entry Mode
tft_cmd(0x11); // sleep out
_delay_ms(120);
tft_cmd(0x29); // display on
_delay_ms(20);
}
// Set the active drawing window in display RAM
void tft_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
tft_cmd(0x2A); // Column Address Set
tft_data(x0 >> 8); tft_data(x0 & 0xFF);
tft_data(x1 >> 8); tft_data(x1 & 0xFF);
tft_cmd(0x2B); // Page Address Set
tft_data(y0 >> 8); tft_data(y0 & 0xFF);
tft_data(y1 >> 8); tft_data(y1 & 0xFF);
tft_cmd(0x2C); // Memory Write
}
// Draw a single pixel
void tft_pixel(uint16_t x, uint16_t y, uint16_t color) {
if (x >= TFT_W || y >= TFT_H) return;
tft_set_window(x, y, x, y);
DC_DATA();
CS_LOW();
spi_write(color >> 8);
spi_write(color & 0xFF);
CS_HIGH();
}
// Fill a rectangle
void tft_fill_rect(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) {
if (x >= TFT_W || y >= TFT_H) return;
if (x + w > TFT_W) w = TFT_W - x;
if (y + h > TFT_H) h = TFT_H - y;
tft_set_window(x, y, x + w - 1, y + h - 1);
DC_DATA();
CS_LOW();
uint8_t hi = color >> 8;
uint8_t lo = color & 0xFF;
uint32_t count = (uint32_t)w * h;
while (count--) {
spi_write(hi);
spi_write(lo);
}
CS_HIGH();
}
// Fill the entire screen
void tft_fill_screen(uint16_t color) {
tft_fill_rect(0, 0, TFT_W, TFT_H, color);
}
// ============================================================
// Color helpers
// ============================================================
#define COLOR(r, g, b) ((((r) & 0xF8) << 8) | (((g) & 0xFC) << 3) | ((b) >> 3))
#define BLACK 0x0000
#define WHITE 0xFFFF
#define RED 0xF800
#define GREEN 0x07E0
#define BLUE 0x001F
#define YELLOW 0xFFE0
#define CYAN 0x07FF
#define MAGENTA 0xF81F
#define ORANGE 0xFD20
// ============================================================
// 5x7 font (uppercase + digits)
// ============================================================
static const uint8_t font5x7[][5] PROGMEM = {
{0x00,0x00,0x00,0x00,0x00}, // ' '
{0x00,0x00,0x5F,0x00,0x00}, // '!'
{0x00,0x07,0x00,0x07,0x00}, // '"'
{0x14,0x7F,0x14,0x7F,0x14}, // '#'
{0x24,0x2A,0x7F,0x2A,0x12}, // '$'
{0x23,0x13,0x08,0x64,0x62}, // '%'
{0x36,0x49,0x55,0x22,0x50}, // '&'
{0x00,0x05,0x03,0x00,0x00}, // '''
{0x00,0x1C,0x22,0x41,0x00}, // '('
{0x00,0x41,0x22,0x1C,0x00}, // ')'
{0x14,0x08,0x3E,0x08,0x14}, // '*'
{0x08,0x08,0x3E,0x08,0x08}, // '+'
{0x00,0x50,0x30,0x00,0x00}, // ','
{0x08,0x08,0x08,0x08,0x08}, // '-'
{0x00,0x60,0x60,0x00,0x00}, // '.'
{0x20,0x10,0x08,0x04,0x02}, // '/'
{0x3E,0x51,0x49,0x45,0x3E}, // '0'
{0x00,0x42,0x7F,0x40,0x00}, // '1'
{0x42,0x61,0x51,0x49,0x46}, // '2'
{0x21,0x41,0x45,0x4B,0x31}, // '3'
{0x18,0x14,0x12,0x7F,0x10}, // '4'
{0x27,0x45,0x45,0x45,0x39}, // '5'
{0x3C,0x4A,0x49,0x49,0x30}, // '6'
{0x01,0x71,0x09,0x05,0x03}, // '7'
{0x36,0x49,0x49,0x49,0x36}, // '8'
{0x06,0x49,0x49,0x29,0x1E}, // '9'
{0x00,0x36,0x36,0x00,0x00}, // ':'
{0x00,0x56,0x36,0x00,0x00}, // ';'
{0x08,0x14,0x22,0x41,0x00}, // '<'
{0x14,0x14,0x14,0x14,0x14}, // '='
{0x00,0x41,0x22,0x14,0x08}, // '>'
{0x02,0x01,0x51,0x09,0x06}, // '?'
{0x32,0x49,0x79,0x41,0x3E}, // '@'
{0x7E,0x11,0x11,0x11,0x7E}, // 'A'
{0x7F,0x49,0x49,0x49,0x36}, // 'B'
{0x3E,0x41,0x41,0x41,0x22}, // 'C'
{0x7F,0x41,0x41,0x22,0x1C}, // 'D'
{0x7F,0x49,0x49,0x49,0x41}, // 'E'
{0x7F,0x09,0x09,0x09,0x01}, // 'F'
{0x3E,0x41,0x49,0x49,0x7A}, // 'G'
{0x7F,0x08,0x08,0x08,0x7F}, // 'H'
{0x00,0x41,0x7F,0x41,0x00}, // 'I'
{0x20,0x40,0x41,0x3F,0x01}, // 'J'
{0x7F,0x08,0x14,0x22,0x41}, // 'K'
{0x7F,0x40,0x40,0x40,0x40}, // 'L'
{0x7F,0x02,0x0C,0x02,0x7F}, // 'M'
{0x7F,0x04,0x08,0x10,0x7F}, // 'N'
{0x3E,0x41,0x41,0x41,0x3E}, // 'O'
{0x7F,0x09,0x09,0x09,0x06}, // 'P'
{0x3E,0x41,0x51,0x21,0x5E}, // 'Q'
{0x7F,0x09,0x19,0x29,0x46}, // 'R'
{0x46,0x49,0x49,0x49,0x31}, // 'S'
{0x01,0x01,0x7F,0x01,0x01}, // 'T'
{0x3F,0x40,0x40,0x40,0x3F}, // 'U'
{0x1F,0x20,0x40,0x20,0x1F}, // 'V'
{0x7F,0x20,0x18,0x20,0x7F}, // 'W'
{0x63,0x14,0x08,0x14,0x63}, // 'X'
{0x03,0x04,0x78,0x04,0x03}, // 'Y'
{0x61,0x51,0x49,0x45,0x43}, // 'Z'
};
// Draw a character at scale (1=tiny, 2=medium, 3=big, ...)
void tft_draw_char(uint16_t x, uint16_t y, char c, uint16_t fg, uint16_t bg, uint8_t scale) {
if (c < ' ' || c > 'Z') c = '?';
uint8_t idx = c - ' ';
for (uint8_t col = 0; col < 5; col++) {
uint8_t bits = pgm_read_byte(&font5x7[idx][col]);
for (uint8_t row = 0; row < 7; row++) {
uint16_t color = (bits & (1 << row)) ? fg : bg;
tft_fill_rect(x + col * scale, y + row * scale, scale, scale, color);
}
}
}
void tft_draw_string(uint16_t x, uint16_t y, const char *s, uint16_t fg, uint16_t bg, uint8_t scale) {
while (*s) {
char c = *s++;
if (c >= 'a' && c <= 'z') c -= 32; // uppercase only in our font
tft_draw_char(x, y, c, fg, bg, scale);
x += 6 * scale;
if (x + 5 * scale >= TFT_W) break;
}
}
// ============================================================
// Main - demo
// ============================================================
int main(void) {
spi_init();
tft_init();
// Black background
tft_fill_screen(BLACK);
// Title bar
tft_fill_rect(0, 0, TFT_W, 40, BLUE);
tft_draw_string(20, 12, "BARE METAL SPI", WHITE, BLUE, 2);
// Color stripes
uint16_t colors[] = {RED, ORANGE, YELLOW, GREEN, CYAN, BLUE, MAGENTA};
uint8_t num = sizeof(colors) / sizeof(colors[0]);
uint16_t stripe_h = 30;
for (uint8_t i = 0; i < num; i++) {
tft_fill_rect(0, 60 + i * stripe_h, TFT_W, stripe_h, colors[i]);
}
// Bottom labels
tft_draw_string(10, 280, "ILI9341 OVER SPI", WHITE, BLACK, 1);
tft_draw_string(10, 295, "ATMEGA328P 16 MHZ", WHITE, BLACK, 1);
// Animated dot bouncing across the title bar
int16_t bx = 4, by = 4, dx = 2;
while (1) {
tft_fill_rect(bx, by, 6, 6, BLUE); // erase
bx += dx;
if (bx <= 0 || bx >= TFT_W - 6) dx = -dx;
tft_fill_rect(bx, by, 6, 6, WHITE); // draw
_delay_ms(30);
}
}