让我从最基础的"为什么"开始讲,建立全景图,再一步步解释每一行代码。
第一步:先理解"全景图"——数据是怎么从代码到屏幕的?
你现在的方式(CPU方式)
你的代码
↓
CPU 亲自拿着每1个字节
↓
交给 SPI 硬件
↓
SPI 发给屏幕问题: 240×320 屏幕 = 76800 个像素,每个像素 2 字节 = 153600 次,CPU 要亲自跑 153600 趟,非常慢。
DMA 方式
你的代码
↓
CPU 把数据放进一个"快递仓库"(DMA缓冲区)
↓
告诉 DMA:"帮我把仓库里的东西全部送给 SPI"
↓
DMA 自动搬运(CPU 可以去休息或做别的事)
↓
SPI 发给屏幕好处: CPU 只需要准备一次数据,剩下的交给 DMA 自动完成。
第二步:为什么需要"特殊的仓库"(DMA缓冲区)?
这是最关键的问题。
ESP32 的内存分两种区域:
┌─────────────────────────────────────┐
│ 普通内存(PSRAM/堆) │
│ CPU 可以用,但 DMA 不能直接访问 │
├─────────────────────────────────────┤
│ DMA专用内存(内部DRAM) │
│ CPU 可以用,DMA 也可以直接访问 ✓ │
└─────────────────────────────────────┘DMA 就像一个只认特定仓库地址的快递员,你必须把货物放在它认识的仓库里,它才能帮你搬运。
所以我们需要用特殊的方式申请内存,告诉系统:"我要的是 DMA 能访问的那种内存"。
第三步:逐行解释那些"看不懂"的代码
关于 heap_caps_malloc
c
uint8_t *dma_buf = heap_caps_malloc(DMA_CHUNK_SIZE, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);把它拆开来看:
heap_caps_malloc = 申请内存的函数(带条件版本)
普通版本是 malloc(),这个是"带要求"的版本
DMA_CHUNK_SIZE = 我要申请多大的内存(字节数)
MALLOC_CAP_DMA = 条件1:这块内存必须是 DMA 能访问的
MALLOC_CAP_INTERNAL = 条件2:必须在芯片内部(不是外部PSRAM)
| 符号 = "并且"的意思,两个条件都要满足用大白话说: "帮我在芯片内部申请一块 DMA 快递员能访问的仓库,大小是 DMA_CHUNK_SIZE 字节。"
关于分块(DMA_CHUNK_SIZE)
为什么要分块?
SPI 硬件一次能搬运的数据有上限,就像快递车一次装不了太多货:
总数据量:153600 字节(整个屏幕)
快递车容量:4096 字节(每次最多搬这么多)
所以需要跑:153600 ÷ 4096 ≈ 38 趟第四步:CPU模式传送像素数据代码
C
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "esp_log.h"
// 引脚定义
#define lcd_spi_cs GPIO_NUM_10 // CS 片选引脚,当CS拉低时表示SPI从机设备被选中,可以进行数据传输
#define lcd_spi_dc GPIO_NUM_14 // DC 数据/命令引脚,拉低表示数据,拉高表示命令
#define lcd_spi_rst GPIO_NUM_13 // RST 复位引脚
#define lcd_spi_bl GPIO_NUM_9 // BL LED 背光引脚
#define lcd_spi_mosi GPIO_NUM_11 // MOSI (也是SDA)主输出引脚,串行数据线用于发送数据到屏幕
#define lcd_spi_clk GPIO_NUM_12 // CLK 时钟引脚
#define spi_host SPI2_HOST // ESP32-S3 SPI 主机控制器选择
spi_device_handle_t spi_device_handle;
// 函数声明
void lcd_gpio_init(void);
void st7789_init(spi_device_handle_t spi_device_handle);
void st7789_write_cmd(uint8_t cmd, spi_device_handle_t spi_device_handle);
void st7789_write_data(uint8_t data, spi_device_handle_t spi_device_handle);
void st7789_set_window(uint16_t x0, uint16_t y0,
uint16_t x1, uint16_t y1,
spi_device_handle_t spi_device_handle);
void lcd_fill_color_cpu(uint16_t color, spi_device_handle_t spi_handle);
void lcd_fill_color_dma(uint16_t color, spi_device_handle_t spi_handle);
/**
* @brief 主函数
*/
void app_main(void)
{
lcd_gpio_init();
// SPI bus initialization
spi_bus_config_t spi_buscfg = {
.miso_io_num = -1,
.mosi_io_num = lcd_spi_mosi,
.sclk_io_num = lcd_spi_clk,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 240 * 320 * 2 + 8,
};
esp_err_t ret = spi_bus_initialize(SPI2_HOST, &spi_buscfg, SPI_DMA_CH_AUTO);
if (ret != ESP_OK) {
ESP_LOGE("ST7789", "Failed to initialize SPI bus");
return;
};
// 配置SPI设备参数结构体
spi_device_interface_config_t spi_st7789_dev_cfg = {
.clock_source = SPI_CLK_SRC_DEFAULT, // 使用默认时钟源
.clock_speed_hz = 40 * 1000 * 1000, // SPI时钟频率,根据屏幕规格书,最高可达62.5MHz,但实际使用中可能需要调整以确保稳定性
.spics_io_num = lcd_spi_cs, // SPI片选引脚
.mode = 0, // SPI模式,CLK polarity: 0, CLK phase: 0,时钟极性说的是空闲状态时的CLK信号电平
.queue_size = 7,
};
// 将SPI设备添加到SPI总线上
ret = spi_bus_add_device(SPI2_HOST, &spi_st7789_dev_cfg, &spi_device_handle);
if (ret != ESP_OK) {
ESP_LOGE("ST7789", "Failed to add SPI device");
return;
}
// 初始化屏幕
st7789_init(spi_device_handle);
// 填充屏幕(0x801F 紫色, 0x07E0 绿色, 0xF800 红色, 0xFFFF 白色)
// lcd_fill_color_cpu(0x07E0, spi_device_handle);
// 定义颜色数组(顺序:红黄蓝黑白绿橙粉棕紫)
uint16_t colors[] = {
0xF800, // 红
0xFFE0, // 黄
0x001F, // 蓝
0x0000, // 黑
0xFFFF, // 白
0x07E0, // 绿
0xFC00, // 橙
0xF81F, // 粉
0x9C60, // 棕
0x801F // 紫
};
int color_count = sizeof(colors) / sizeof(colors[0]); // 获取颜色数组的长度
int index = 0; // 颜色索引
// 循环填充颜色(10种颜色)
while (1) {
lcd_fill_color_cpu(colors[index], spi_device_handle); // 调用填充颜色函数,发送颜色数据给屏幕)
index = (index + 1) % color_count; // 发送完一种颜色后,将索引加1,并取余数,确保索引在颜色数组范围内
vTaskDelay(pdMS_TO_TICKS(500)); // 延时0.5秒
}
}
/**
* @brief GPIO初始化
*/
void lcd_gpio_init(void){
// GPIO配置结构体
gpio_config_t lcd_gpio_config = {
.pin_bit_mask = (1ULL << lcd_spi_dc) | (1ULL << lcd_spi_rst) | (1ULL << lcd_spi_bl),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
// GPIO初始化
gpio_config(&lcd_gpio_config);
// 初始化完成后,默认将DC、RST、BL引脚设置为高电平,确保屏幕处于非复位状态并且背光打开
gpio_set_level(lcd_spi_dc, 1); // 默认数据模式
gpio_set_level(lcd_spi_rst, 1); // 默认复位状态
gpio_set_level(lcd_spi_bl, 1); // 默认背光开启
}
/**
* @brief 发送命令
*
* @param cmd 命令
* @param spi_handle SPI句柄
*/
void st7789_write_cmd(uint8_t cmd, spi_device_handle_t spi_device_handle)
{
// 引脚拉低,表示command,Page48 D/CX indicates whether the byte is command (D/CX=’0’)
// or parameter/RAM data (D/CX=’1’).
gpio_set_level(lcd_spi_dc, 0);
// 发送命令
spi_transaction_t transmit_cmd_config = {
.tx_buffer = &cmd,
.length = 8,
};
spi_device_polling_transmit(spi_device_handle, &transmit_cmd_config);
}
/**
* @brief 发送数据
*
* @param data 数据
* @param spi_handle SPI句柄
*/
void st7789_write_data(uint8_t data, spi_device_handle_t spi_device_handle)
{
// 引脚拉高,表示数据,Page48 D/CX indicates whether the byte is command (D/CX=’0’)
// or parameter/RAM data (D/CX=’1’).
gpio_set_level(lcd_spi_dc, 1);
// 发送数据
spi_transaction_t transmit_data_config = {
.tx_buffer = &data,
.length = 8,
};
spi_device_polling_transmit(spi_device_handle, &transmit_data_config);
}
/**
* @brief SPI初始化,配置SPI总线参数并添加SPI设备
*
*/
void st7789_init(spi_device_handle_t spi_device_handle){
// 1, 硬件复位,拉低RST引脚
// 屏幕初始化代码
gpio_set_level(lcd_spi_rst, 0); // 硬件复位,拉低RST引脚,默认高电平
// 延时10ms,Page42 ↓
// 原文 7. It is necessary to wait 5msec after releasing RESX before sending commands. Also Sleep Out command cannot be sent for 120msec.
vTaskDelay(pdMS_TO_TICKS(10));
gpio_set_level(lcd_spi_rst, 1); // 硬件复位,拉高,恢复高电平
vTaskDelay(pdMS_TO_TICKS(120));
// 延时120ms,Page42 ↓
// 原文 3. During the Resetting period, the display will be blanked
// (The display is entering blanking sequence, which maximum time is 120 ms, when Reset Starts in Sleep Out –mode.
// 2, 软件复位,发送0x01命令
// 发送软件复位命令0x01,Page124
st7789_write_cmd(0x01, spi_device_handle);
vTaskDelay(pdMS_TO_TICKS(120));
// 3, 退出睡眠模式,发送0x11命令
st7789_write_cmd(0x11, spi_device_handle);
vTaskDelay(pdMS_TO_TICKS(10)); // 从睡眠模式退出后并立即再次进入睡眠模式要等120毫秒,而退出后发送其他新命令要等待至少5毫秒
// 4, 设置像素格式,发送0x3A命令,参数0x55表示16位颜色以及262K,Page185,
st7789_write_cmd(0x3A, spi_device_handle);
st7789_write_data(0x55, spi_device_handle);
// 5,设置显示方向,发送0x36命令,参数0x00表示竖屏、RGB模式,Page176
st7789_write_cmd(0x36, spi_device_handle);
st7789_write_data(0x00, spi_device_handle);
// 6, 设置颜色反转开启 0x21
st7789_write_cmd(0x21, spi_device_handle);
// 7,设置display on,发送0x29命令,Page124
st7789_write_cmd(0x29, spi_device_handle);
vTaskDelay(pdMS_TO_TICKS(100)); // 等待100ms,确保屏幕稳定显示
ESP_LOGI("ST7789", "ST7789 init success!");
}
/**
* @brief 设置 ST7789 的显示窗口(列/行地址范围)
* @param x0 起始列(0~239)
* @param y0 起始行(0~319)
* @param x1 结束列(0~239)
* @param y1 结束行(0~319)
* @param spi_handle SPI 设备句柄
*/
void st7789_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, spi_device_handle_t spi_handle)
{
// ---- 设置列地址(X 方向) ----
st7789_write_cmd(0x2A, spi_handle); // 告诉屏幕:接下来我要发列地址参数
// 发送起始列(高字节,低字节)
st7789_write_data((x0 >> 8) & 0xFF, spi_handle); // 起始列的高 8 位
st7789_write_data(x0 & 0xFF, spi_handle); // 起始列的低 8 位
// 发送结束列(高字节,低字节)
st7789_write_data((x1 >> 8) & 0xFF, spi_handle); // 结束列的高 8 位
st7789_write_data(x1 & 0xFF, spi_handle); // 结束列的低 8 位
// ---- 设置行地址(Y 方向) ----
st7789_write_cmd(0x2B, spi_handle); // 告诉屏幕:接下来我要发行地址参数
// 发送起始行(高字节,低字节)
st7789_write_data((y0 >> 8) & 0xFF, spi_handle);
st7789_write_data(y0 & 0xFF, spi_handle);
// 发送结束行(高字节,低字节)
st7789_write_data((y1 >> 8) & 0xFF, spi_handle);
st7789_write_data(y1 & 0xFF, spi_handle);
}
/**
* @brief 填充整个屏幕为指定颜色(RGB565)
* @param color 16位颜色值
* @param spi_device_handle SPI设备句柄
*/
void lcd_fill_color_cpu(uint16_t color, spi_device_handle_t spi_device_handle)
{
// 1. 设置全屏窗口 (0,0) ~ (239,319)
st7789_set_window(0, 0, 239, 319, spi_device_handle);
// 2. 发送写内存命令 (0x2C)
st7789_write_cmd(0x2C, spi_device_handle);
// 3. 计算像素总数
uint32_t pixel_count = 240 * 320; // 76800 个像素
// 4. 逐个字节发送颜色数据(每个像素2字节)
uint8_t color_high = (color >> 8) & 0xFF;
uint8_t color_low = color & 0xFF;
for (uint32_t i = 0; i < pixel_count; i++) {
st7789_write_data(color_high, spi_device_handle);
st7789_write_data(color_low, spi_device_handle);
}
}
/**
* @brief 填充整个屏幕为指定颜色(DMA)
* @param color 16位颜色值
* @param spi_device_handle SPI设备句柄
*/
// void lcd_fill_color_dma(uint16_t color, spi_device_handle_t spi_handle)
// {
// // 第1步:设置全屏窗口
// st7789_set_window(0, 0, 239, 319, spi_handle);
// // 第2步:发送写内存命令
// st7789_write_cmd(0x2C, spi_handle);
// // 第3步:DC 引脚拉高(数据模式)
// gpio_set_level(lcd_spi_dc, 1);
// // 第4步:申请一块 DMA 内存作为"快递车"
// // 不能一次发整屏,硬件有单次传输上限,所以分块发送
// int chunk_size = 4096; // 每次发 4096 字节(约 2048 个像素)
// uint8_t *buf = heap_caps_malloc(chunk_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
// if (buf == NULL) {
// ESP_LOGE("ST7789", "DMA内存申请失败!");
// return;
// }
// // 第5步:把颜色数据填满缓冲区
// uint8_t hi = (color >> 8) & 0xFF;
// uint8_t lo = color & 0xFF;
// for (int i = 0; i < chunk_size; i += 2) {
// buf[i] = hi;
// buf[i + 1] = lo;
// }
// // 第6步:循环分块发送,直到整屏填满
// int total = 240 * 320 * 2; // 153600 字节
// int sent = 0;
// while (sent < total) {
// // 计算本次发多少字节(最后一块可能不足 4096)
// int this_time = total - sent;
// if (this_time > chunk_size) this_time = chunk_size;
// spi_transaction_t t = {
// .tx_buffer = buf,
// .length = this_time * 8, // 单位是 bit,所以 ×8
// };
// spi_device_transmit(spi_handle, &t);
// sent += this_time;
// }
// // 第7步:释放内存
// heap_caps_free(buf);
// }
// void lcd_fill_color_dma(uint16_t color, spi_device_handle_t spi_handle)
// {
// st7789_set_window(0, 0, 239, 319, spi_handle);
// st7789_write_cmd(0x2C, spi_handle);
// gpio_set_level(lcd_spi_dc, 1);
// // chunk_size 最大设为 32768(ESP32-S3 硬件单次 DMA 上限)
// int chunk_size = 32768;
// uint8_t *buf = heap_caps_malloc(chunk_size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);
// if (buf == NULL) {
// ESP_LOGE("ST7789", "DMA内存申请失败!");
// return;
// }
// uint8_t hi = (color >> 8) & 0xFF;
// uint8_t lo = color & 0xFF;
// for (int i = 0; i < chunk_size; i += 2) {
// buf[i] = hi;
// buf[i + 1] = lo;
// }
// int total = 240 * 320 * 2; // 153600 字节
// int sent = 0;
// while (sent < total) {
// int this_time = total - sent;
// if (this_time > chunk_size) this_time = chunk_size;
// spi_transaction_t t = {
// .tx_buffer = buf,
// .length = this_time * 8,
// };
// spi_device_transmit(spi_handle, &t);
// sent += this_time;
// }
// heap_caps_free(buf);
// }