1. ILI9341 与 SPI 显示基础

ILI9341 是一款常见的 TFT-LCD 驱动控制器,支持 SPI、并行等接口,其中 SPI 模式因引脚少、速度快而广泛用于嵌入式设备。SPI 接口下,ILI9341 作为从设备,STM32 作为主机,通过 MOSI、SCK、CS、DC(数据/命令选择)、RST 等引脚进行通信。

关键点:

  • ILI9341 内部有显存(GRAM),大小为 240x320 像素,每像素 16 位(RGB565)。
  • 写命令时 DC 引脚拉低,写数据时 DC 拉高。
  • SPI 模式支持 Mode 0(CPOL=0, CPHA=0)和 Mode 3,通常使用 Mode 0。
  • 最大 SPI 时钟可达 10MHz 以上,但需根据布线调整。

2. 硬件连接与 SPI 配置

2.1 引脚连接(以 STM32F103 为例)

| STM32 引脚 | LCD 引脚 | 说明 | |------------|----------|------| | PA5 | SCK | SPI 时钟 | | PA7 | MOSI | 主出从入 | | PA4 | CS | 片选(低有效) | | PA6 | DC | 数据/命令选择 | | PA3 | RST | 复位(低有效) | | 3.3V | VCC | 电源 | | GND | GND | 地 |

注意:DC 和 RST 可用普通 GPIO 控制,不占用 SPI 外设。

2.2 SPI 初始化(HAL 库)

使用 STM32CubeMX 配置 SPI1,模式为 Full-Duplex Master,数据大小 8 位,时钟极性 Low,相位 1Edge,预分频 16(即 8MHz)。生成代码后,在 main.c 中补充 DC 和 RST 的 GPIO 初始化。

// SPI 句柄声明(CubeMX 已生成)
extern SPI_HandleTypeDef hspi1;

// DC 和 RST 引脚定义(根据实际接线)
#define LCD_DC_PIN       GPIO_PIN_6
#define LCD_DC_PORT      GPIOA
#define LCD_RST_PIN      GPIO_PIN_3
#define LCD_RST_PORT     GPIOA

// 简单宏定义命令/数据切换
#define LCD_DC_LOW()     HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_RESET)
#define LCD_DC_HIGH()    HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_SET)

3. ILI9341 初始化序列

ILI9341 上电后需发送一系列初始化命令,设置显示参数。以下为常用序列(摘自官方数据手册):

void LCD_Init(void) {
    // 硬件复位
    HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_RESET);
    HAL_Delay(20);
    HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_SET);
    HAL_Delay(120);

    // 发送初始化命令(部分)
    LCD_WriteCmd(0xCF); LCD_WriteData(0x00); LCD_WriteData(0xC1); LCD_WriteData(0x30);
    LCD_WriteCmd(0xED); LCD_WriteData(0x64); LCD_WriteData(0x03); LCD_WriteData(0x12); LCD_WriteData(0x81);
    LCD_WriteCmd(0xE8); LCD_WriteData(0x85); LCD_WriteData(0x10); LCD_WriteData(0x7A);
    LCD_WriteCmd(0xCB); LCD_WriteData(0x39); LCD_WriteData(0x2C); LCD_WriteData(0x00); LCD_WriteData(0x34); LCD_WriteData(0x02);
    LCD_WriteCmd(0xF7); LCD_WriteData(0x20);
    LCD_WriteCmd(0xEA); LCD_WriteData(0x00); LCD_WriteData(0x00);
    LCD_WriteCmd(0xC0); LCD_WriteData(0x1B);
    LCD_WriteCmd(0xC1); LCD_WriteData(0x12);
    LCD_WriteCmd(0xC5); LCD_WriteData(0x32); LCD_WriteData(0x3C);
    LCD_WriteCmd(0xC7); LCD_WriteData(0x91);
    LCD_WriteCmd(0x36); LCD_WriteData(0x48); // 内存访问控制,设置扫描方向
    LCD_WriteCmd(0x3A); LCD_WriteData(0x55); // 像素格式为 RGB565
    LCD_WriteCmd(0xB1); LCD_WriteData(0x00); LCD_WriteData(0x1B);
    LCD_WriteCmd(0xB6); LCD_WriteData(0x0A); LCD_WriteData(0xA2);
    LCD_WriteCmd(0xF2); LCD_WriteData(0x00);
    LCD_WriteCmd(0x26); LCD_WriteData(0x01);
    LCD_WriteCmd(0xE0); // 设置 Gamma 曲线(略)
    // ... 更多 Gamma 参数
    LCD_WriteCmd(0x11); // 退出睡眠
    HAL_Delay(120);
    LCD_WriteCmd(0x29); // 开启显示
}

说明: 实际初始化序列需根据屏幕厂商提供的数据手册调整,但核心命令如 0x3A(像素格式)、0x36(扫描方向)、0x11、0x29 必须正确。

4. 底层写命令与写数据函数

// 发送命令
void LCD_WriteCmd(uint8_t cmd) {
    LCD_DC_LOW();
    HAL_SPI_Transmit(&hspi1, &cmd, 1, 100);
}

// 发送数据
void LCD_WriteData(uint8_t data) {
    LCD_DC_HIGH();
    HAL_SPI_Transmit(&hspi1, &data, 1, 100);
}

// 连续发送数据(用于填充像素)
void LCD_WriteDataBuffer(uint8_t *buf, uint16_t len) {
    LCD_DC_HIGH();
    HAL_SPI_Transmit(&hspi1, buf, len, 1000);
}

5. 设置显示窗口与画点

ILI9341 支持设置矩形窗口,之后写入像素数据即可填充该区域。

// 设置窗口(x0,y0 左上角,x1,y1 右下角)
void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) {
    LCD_WriteCmd(0x2A); // 列地址设置
    LCD_WriteData(x0 >> 8); LCD_WriteData(x0 & 0xFF);
    LCD_WriteData(x1 >> 8); LCD_WriteData(x1 & 0xFF);
    LCD_WriteCmd(0x2B); // 行地址设置
    LCD_WriteData(y0 >> 8); LCD_WriteData(y0 & 0xFF);
    LCD_WriteData(y1 >> 8); LCD_WriteData(y1 & 0xFF);
    LCD_WriteCmd(0x2C); // 开始写入显存
}

// 画一个像素(颜色为 RGB565)
void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color) {
    if (x >= 240 || y >= 320) return;
    LCD_SetWindow(x, y, x, y);
    uint8_t data[2] = { color >> 8, color & 0xFF };
    LCD_WriteDataBuffer(data, 2);
}

// 填充整个屏幕
void LCD_FillScreen(uint16_t color) {
    LCD_SetWindow(0, 0, 239, 319);
    uint32_t pixels = 240 * 320;
    uint8_t *buf = (uint8_t*)malloc(pixels * 2);
    if (buf) {
        for (uint32_t i = 0; i < pixels; i++) {
            buf[i*2] = color >> 8;
            buf[i*2+1] = color & 0xFF;
        }
        LCD_WriteDataBuffer(buf, pixels * 2);
        free(buf);
    } else {
        // 内存不足时逐像素发送(较慢)
        for (uint32_t i = 0; i < pixels; i++) {
            uint8_t d[2] = { color >> 8, color & 0xFF };
            LCD_WriteDataBuffer(d, 2);
        }
    }
}

6. 完整示例:点亮屏幕并显示彩色条纹

#include "main.h"
#include "spi.h"
#include "gpio.h"

// 函数声明
void LCD_Init(void);
void LCD_WriteCmd(uint8_t cmd);
void LCD_WriteData(uint8_t data);
void LCD_WriteDataBuffer(uint8_t *buf, uint16_t len);
void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1);
void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color);
void LCD_FillScreen(uint16_t color);

int main(void) {
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_SPI1_Init();

    LCD_Init();
    LCD_FillScreen(0x001F); // 蓝色
    HAL_Delay(1000);
    LCD_FillScreen(0x07E0); // 绿色
    HAL_Delay(1000);
    LCD_FillScreen(0xF800); // 红色
    HAL_Delay(1000);

    // 画一条对角线
    for (int i = 0; i < 240; i++) {
        LCD_DrawPixel(i, i, 0xFFFF); // 白色
    }

    while (1) {
        // 循环
    }
}

7. 性能优化与注意事项

  • SPI 时钟频率:不要盲目提高,若屏幕出现花屏,降低分频系数。
  • DMA 传输:对于大量像素填充,建议使用 SPI DMA,可大幅提升刷新率。
  • 内存管理:填充全屏时避免一次性分配大缓冲区,可分段发送。
  • 电源稳定性:LCD 背光电流较大,确保电源足够,否则可能闪烁。
  • 扫描方向:通过 0x36 命令的 MADCTL 位控制,不同屏幕安装方向需调整。
  • 初始化序列:不同厂商的 ILI9341 模块可能有细微差异,务必参考模块资料。

8. 总结

本文从原理到实践,详细介绍了 STM32 通过 SPI 驱动 ILI9341 屏幕的方法。掌握这些基础后,你可以进一步实现字符显示、图片显示,甚至移植 LVGL 等 GUI 库。关键在于理解命令/数据时序和窗口设置,并注意硬件细节。希望这篇文章能帮你快速上手,开启嵌入式显示开发之旅。