基于STM32F407ZET6 实现1.69寸TFT LCD ST7789驱动设计
STM32F4系列 st7789驱动 V2.0 开启DMA实现中文、图片显示(STM32CubeMX+Keil)
文章目录
前言
一、STM32CubeMX配置
1.SPI开启DMA配置
二、软件实现
1. ST7789.c相关函数介绍
2. Fonts.c相关函数介绍
3. 字模软件ATK_XFONT.exe使用配置
4.图片取模软件 Img2Lcd.exe使用配置
5. main.c测试实现
三、 烧录测试验证
前言
在基于 STM32 的项目开发中,为 SPI 接口的屏幕驱动开启 DMA(直接存储器访问)功能,可显著提升屏幕刷新率;同时,在屏幕上实现中文和图片的流畅显示,能大幅增强项目的人机交互可读性与视觉体验,是嵌入式界面开发中兼顾性能与易用性的关键优化手段。
一、DMA 核心知识解析
DMA(Direct Memory Access,直接存储器访问)是 STM32 芯片的核心外设之一,其核心作用是在不占用 CPU 核心的前提下,实现存储器(RAM/FLASH)与外设(如 SPI、UART、I2C)之间的数据直接传输—— 形象地说,DMA 就像一个独立的 “数据搬运工”,能自主把存储器中的屏幕显示数据(如像素、中文字模、图片位图)直接搬运到 SPI 外设并发送至屏幕,全程无需 CPU 干涉。
- 传统传输模式(CPU 轮询 / 中断):CPU 需要主动参与每一次数据读写 —— 先从存储器取数据,再通过 SPI 发送给屏幕,全程占用 CPU 资源,若屏幕数据量较大(如整屏图片),CPU 会被频繁占用,导致刷新率低、响应延迟。
- DMA 传输模式:CPU 仅需初始化 DMA 传输参数(如数据源地址、目标外设、传输长度),后续数据传输由 DMA 控制器这个 “搬运工” 独立完成,传输完成后通过中断通知 CPU(可选),CPU 可在传输期间处理其他任务(如逻辑运算、传感器数据采集)。
通俗来说 DMA 如同 “独立数据搬运工”,脱离 CPU 独立完成 SPI 数据传输,核心是 “解放 CPU + 提升传输效率”。
二、软件工具
1.字模软件ATK_XFONT.exe
下载地址:字模软件(ATK-XFONT) 版本:v2.0.3 — 正点原子资料下载中心 1.0.0 文档 (openedv.com)

2.图片取模软件 Img2Lcd.exe
链接:image2lcdV4.0:嵌入式图片转换工具,高效适配LCD显示需求 - AtomGit | GitCode

附录:软件压缩包
一、SPI开启DMA配置
1.CubeMX 开启 SPI DMA 操作步骤:
- 打开 SPI 外设配置在 Pinout & Configuration 界面,找到
SPI1(这里使用SPI3为例),在 Mode 中选择Full-Duplex Master/Slave等模式。 - 切换到标签页,然后点击 Add 按钮添加 DMA 流。
- 选择 DMA 请求与流
DMA Request:选择SPI3_TX(对应 SPI 发送请求,STM32F407为主机,屏幕为从机,需要DMA把内存数据发送给屏幕RAM)Stream:可以保持默认也可下拉选项,选择可用流
- 配置下方 DMA Request Settings以及相关配置说明
- DMA 中断(默认开启)本文未使用
- 生成代码点击
GENERATE CODE,CubeMX 会自动生成MX_DMA_Init()和MX_SPI1_Init()函数,完成 DMA 与 SPI 的底层绑定。
| 参数项 | 你的配置 | 含义与作用 |
|---|---|---|
| DMA Request | SPI3_TX |
指定 DMA 要响应的外设请求,这里是 SPI1 的发送请求,即 SPI 发送数据时触发 DMA 搬运。 |
| Stream | DMA1 Stream5 |
DMA 控制器的数据流通道,不同 STM32 系列(如 F4/H7)的流与请求映射不同,需参考数据手册确认该流是否支持 SPI1_TX。 |
| Direction | Memory To Peripheral |
数据从 内存(显存 / 字库 / 图片) 搬运到 SPI 外设寄存器,符合屏幕驱动场景。 |
| Priority | High |
DMA 传输优先级,可根据系统需求调整为 High/Very High,屏幕刷新通常设为 High以上避免卡顿。 |
| Mode | Normal |
传输完成后 DMA 自动停止,需手动重新触发下一次传输;若为 Circular 则会循环传输(适合音频等场景,比如MX98357后续会涉及)。 |
| Increment Address - Peripheral | 未勾选 | SPI 数据寄存器地址固定,不需要自增,否则会写向错误地址。 |
| Increment Address - Memory | 勾选 | 内存地址需要逐字节递增,才能连续发送整段像素数据。 |
| Use FIFO | 未勾选 | 是否启用 DMA FIFO 缓冲,关闭时直接传输;高分辨率屏幕可开启以优化吞吐量。 |
| Data Width - Peripheral/Memory | Byte |
单次传输数据宽度,与四线SPI 数据帧格式默认8bit ,因此这里选择Byte。 |

二、软件实现
1.ST7789.c相关函数介绍
1. 编译阶段:DMA 功能开关(宏定义控制)支持在头文件中修改是否启用DMA
#ifdef USE_DMA
// DMA相关缓冲区、参数定义
uint16_t DMA_MIN_SIZE = 16; // DMA传输最小数据量阈值
#define HOR_LEN 10 // 分块高度(适配小RAM场景)
uint16_t disp_buf[ST7789_WIDTH * HOR_LEN]; // DMA帧缓冲区
#endif
核心作用:通过USE_DMA宏开关控制是否编译 DMA 相关代码,实现 “DMA / 非 DMA 模式” 一键切换;
缓冲区设计:disp_buf是 DMA 传输的核心载体,大小为屏幕宽度 × 分块高度(如 240×10),而非整屏(240×280),目的是适配 RAM 不足的 MCU(比如 STM32F103 仅有 20KB RAM,整屏缓冲区需 131KB,无法容纳)。
2.初始化阶段:DMA 缓冲区准备
void ST7789_Init(void)
{
#ifdef USE_DMA
memset(disp_buf, 0, sizeof(disp_buf)); // 初始化DMA帧缓冲区
#endif
// ... 屏幕硬件复位、寄存器配置(详情可参考V1.0版)
ST7789_Fill_Color(BLACK); // 全屏填充
}
核心动作:初始化时将 DMA 缓冲区清零,避免脏数据导致屏幕显示异常;
关联逻辑:初始化最后调用ST7789_Fill_Color,首次触发 DMA 传输流程,验证 DMA 功能是否正常
3. 数据传输阶段:DMA / 阻塞传输自适应切换
PS:开启DMA色彩异常,红蓝色彩显示异常需开启字节交换
static void ST7789_WriteData(uint8_t *buff, size_t buff_size)
{
ST7789_Select();
ST7789_DC_Set(); // 数据模式
// 分块发送(HAL SPI单次最大传输64KB)
while (buff_size > 0) {
uint16_t chunk_size = buff_size > 65535 ? 65535 : buff_size;
#ifdef USE_DMA
// 数据量≥阈值时用DMA,否则用阻塞传输
if (DMA_MIN_SIZE <= buff_size)
{
HAL_SPI_Transmit_DMA(&ST7789_SPI_PORT, buff, chunk_size);
// 等待DMA传输完成(轮询DMA状态)
while (ST7789_SPI_PORT.hdmatx->State != HAL_DMA_STATE_READY)
{}
}
else
HAL_SPI_Transmit(&ST7789_SPI_PORT, buff, chunk_size, HAL_MAX_DELAY);
#else
HAL_SPI_Transmit(&ST7789_SPI_PORT, buff, chunk_size, HAL_MAX_DELAY);
#endif
buff += chunk_size;
buff_size -= chunk_size;
}
ST7789_UnSelect();
}
核心分支:当数据量≥DMA_MIN_SIZE(16 字节):调用HAL_SPI_Transmit_DMA启动 DMA 传输;当数据量 < 16 字节:用阻塞传输(HAL_SPI_Transmit),避免 DMA 初始化开销大于传输收益;
关键细节:双字节交换解决DMA红蓝反色问题,轮询等待:while (ST7789_SPI_PORT.hdmatx->State != HAL_DMA_STATE_READY) 确保当前块传输完成后再发下一块,避免数据错乱。
4.功能使用:分块填充 + DMA 传输(以全屏填充为例)
void ST7789_Fill_Color(uint16_t color)
{
ST7789_SetAddressWindow(0, 0, ST7789_WIDTH - 1, ST7789_HEIGHT - 1);
ST7789_Select();
// LCD 期望的是高字节在前(大端),所以必须手动交换字节顺序 预先计算字节交换后的颜色
uint16_t swapped_color = (color >> 8) | (color << 8);
#ifdef USE_DMA
// ------------------- DMA模式:分块填充缓冲区传输 -------------------
for (uint16_t i = 0; i < ST7789_HEIGHT / HOR_LEN; i++)
{
for (uint32_t j = 0; j < (ST7789_WIDTH * HOR_LEN); j++)
{
disp_buf[j] = swapped_color;
}
ST7789_WriteData((uint8_t *)disp_buf, sizeof(disp_buf));
}// 分块填充缓冲区:整屏拆分为 N 个 HOR_LEN 高度的小块
#else
// ------------------- 非DMA模式:逐像素写入 -------------------
uint16_t i,j;
for (i = 0; i < ST7789_WIDTH; i++)
for (j = 0; j < ST7789_HEIGHT; j++) {
uint8_t data[] = {color >> 8, color & 0xFF}; // 将16位颜色拆分为高低字节
ST7789_WriteData(data, sizeof(data)); // 单像素数据传输
}
#endif
ST7789_UnSelect();
}
核心思路:因 RAM 不足无法存储整屏数据,将屏幕按高度拆分为多个小块(比如 280 高度拆为 28 个 10 行的块,适当调试可以自定义修改);
传输逻辑:填充一块、DMA 传输一块,循环直至整屏完成,既利用 DMA 提升效率,又规避 RAM 不足问题。
5.核心功能函数:ST7789_DrawImage:绘制 RGB565 位图图像
void ST7789_DrawImage(uint16_t x, uint16_t y, uint16_t w, uint16_t h, const unsigned char *data)
{
// 1. 边界校验:过滤无效参数,截断超出屏幕的图像区域
if (x >= ST7789_WIDTH || y >= ST7789_HEIGHT || w == 0 || h == 0) return;
uint16_t draw_w = (x + w) > ST7789_WIDTH ? (ST7789_WIDTH - x) : w; // 实际绘制宽度
uint16_t draw_h = (y + h) > ST7789_HEIGHT ? (ST7789_HEIGHT - y) : h; // 实际绘制高度
if (draw_w == 0 || draw_h == 0) return;
// 2. 设置显示窗口:仅选中屏幕、设置窗口后立即取消,避免和WriteData的片选冲突
ST7789_Select();
ST7789_SetAddressWindow(x, y, x + draw_w - 1, y + draw_h - 1); // 窗口范围:左上→右下
ST7789_UnSelect();
// 3. 计算有效数据长度:只传输屏幕内的像素数据,避免无效传输
uint32_t valid_pixel = draw_w * draw_h;
uint32_t valid_byte = valid_pixel * 2; // RGB565每个像素2字节
uint32_t total_byte = w * h * 2;
if (valid_byte > total_byte) valid_byte = total_byte;
// 4. 分块传输:HAL SPI的DMA/阻塞传输最大支持65535字节,拆分数据避免超限
const unsigned char *data_ptr = data;
#define TRANS_CHUNK 65535
while (valid_byte > 0)
{
uint16_t chunk = valid_byte > TRANS_CHUNK ? TRANS_CHUNK : valid_byte;
ST7789_WriteData((uint8_t *)data_ptr, chunk); // 调用底层DMA/阻塞传输
data_ptr += chunk;
valid_byte -= chunk;
}
}
边界截断:比如图像坐标x=200、w=100,而屏幕宽度仅 240,则draw_w=40,只绘制图像的前 40 个像素宽度,避免越界显示乱码;
片选独立处理:SetAddressWindow(设置显示窗口)需要单独操作片选,若和WriteData共用片选会导致命令 / 数据传输冲突;
64KB 分块:HAL 库中HAL_SPI_Transmit_DMA的传输长度是uint16_t类型(最大 65535),拆分后可支持任意大小的图像传输(比如 240×280 的图像≈134KB,拆分为 2 块传输)
ST7789_DrawCNChar_16x16:绘制单个 16×16 中文字符
void ST7789_DrawCNChar_16x16(uint16_t x, uint16_t y, uint16_t char_idx, const CN_FontDef *font, uint16_t color, uint16_t bgcolor)
{
// 1. 严格的参数校验:避免空指针、坐标越界
if (font == NULL || x >= ST7789_WIDTH || y >= ST7789_HEIGHT ||
(x + font->cn_width) > ST7789_WIDTH || (y + font->cn_height) > ST7789_HEIGHT) {
return;
}
// 2. 获取点阵数据:从字库中读取对应字符的16×16点阵(每行2字节,共32字节)
const uint8_t *cn_data = Font_GetCNData(font, char_idx);
if (cn_data == NULL) return;
// 3. 设置字符显示窗口:仅显示当前字符的16×16区域
ST7789_Select();
ST7789_SetAddressWindow(x, y, x + font->cn_width - 1, y + font->cn_height - 1);
// 4. 逐行解析点阵,生成RGB565像素数据
uint8_t pixel_data[2]; // 存储单个像素的RGB565数据(高字节+低字节)
for (uint8_t row = 0; row < font->cn_height; row++) {
uint8_t byte_h = cn_data[row * 2]; // 每行高8位点阵
uint8_t byte_l = cn_data[row * 2 + 1]; // 每行低8位点阵
// 解析高字节8个像素
for (uint8_t col = 0; col < 8; col++) {
// 点阵位为1→显示字符色,0→显示背景色
pixel_data[0] = (byte_h & (0x80 >> col)) ? (color >> 8) : (bgcolor >> 8); // RGB565高字节
pixel_data[1] = (byte_h & (0x80 >> col)) ? (color & 0xFF) : (bgcolor & 0xFF); // RGB565低字节
ST7789_WriteData(pixel_data, 2); // 发送单个像素数据
}
// 解析低字节8个像素(逻辑同上)
for (uint8_t col = 0; col < 8; col++) {
pixel_data[0] = (byte_l & (0x80 >> col)) ? (color >> 8) : (bgcolor >> 8);
pixel_data[1] = (byte_l & (0x80 >> col)) ? (color & 0xFF) : (bgcolor & 0xFF);
ST7789_WriteData(pixel_data, 2);
}
}
ST7789_UnSelect(); // 释放片选
}
点阵数据格式:16×16 中文字符的点阵共 256 位(32 字节),每行占 2 字节(16 位),对应 16 个像素;
RGB565 字节拆分:ST7789 屏幕要求 RGB565 数据 “高字节在前”,因此将color(16 位)拆分为高 8 位(color>>8)和低 8 位(color&0xFF)分别发送
ST7789_DrawCNString_16x16:绘制连续中文字符串
void ST7789_DrawCNString_16x16(uint16_t x, uint16_t y, uint16_t *char_idx, uint8_t count, const CN_FontDef *font, uint16_t color, uint16_t bgcolor)
{
// 空指针/无效参数校验
if (char_idx == NULL || font == NULL || count == 0) return;
uint16_t current_x = x; // 当前绘制的X坐标
for (uint8_t i = 0; i < count; i++) {
// 绘制第i个字符
ST7789_DrawCNChar_16x16(current_x, y, char_idx[i], font, color, bgcolor);
// 字符间距:每个字符后偏移“字符宽度+2像素”,避免字符重叠
current_x += font->cn_width + 2;
// 自动换行:当前X坐标+字符宽度超出屏幕→换行
if (current_x + font->cn_width > ST7789_WIDTH) {
current_x = x; // 回到起始X坐标
y += font->cn_height + 2; // Y坐标下移(字符高度+2像素行间距)
// 超出屏幕高度→停止绘制
if (y + font->cn_height > ST7789_HEIGHT) break;
}
}
}
字符索引数组:char_idx是自定义的字符索引(比如{0,1}对应 “你好”),需提前在字库中定义索引和字符的映射关系;
间距设计:+2像素的行 / 列间距是为了提升文字可读性,避免字符挤在一起;
换行逻辑:换行后回到初始 X 坐标,Y 坐标下移,若下移后超出屏幕高度则直接终止绘制,避免无效操作。
2.Fonts.c相关函数介绍
1. 中文字库点阵数据定义:cn_font_16x16_data
static const uint8_t cn_font_16x16_data[] = {
// “你”的16×16点阵数据(共32字节,每行2字节,16行)
0x08,0x80,0x08,0x80,0x08,0x80,0x11,0xFE,0x11,0x02,0x32,0x04,0x34,0x20,0x50,0x20,
0x91,0x28,0x11,0x24,0x12,0x24,0x12,0x22,0x14,0x22,0x10,0x20,0x10,0xA0,0x10,0x40,//你0
// “好”的16×16点阵数据(共32字节)
0x10,0x00,0x10,0xFC,0x10,0x04,0x10,0x08,0xFC,0x10,0x24,0x20,0x24,0x20,0x25,0xFE,
0x24,0x20,0x48,0x20,0x28,0x20,0x10,0x20,0x28,0x20,0x44,0x20,0x84,0xA0,0x00,0x40,//好1
};
数据格式:16×16 点阵字符需要
16×16 = 256位 = 32字节存储,因此 “你” 和 “好” 各占 32 字节,总长度 64 字节;
2. 字体结构体定义与实例:CN_FontDef
/** 中文字体结构体(16x16) */
typedef struct {
uint8_t cn_width; // 中文字符宽度(固定16)
uint8_t cn_height; // 中文字符高度(固定16)
const uint8_t *cn_data; // 中文字模数据指针
uint16_t cn_count; // 中文字符数量
} CN_FontDef;
// 16x16 中文字体实例
const CN_FontDef CN_Font_16x16 = {
.cn_width = 16, // 字符宽度16像素
.cn_height = 16, // 字符高度16像素
.cn_data = cn_font_16x16_data, // 指向上面的点阵数据
.cn_count = 2 // 包含“你”“好”2个字符
};
// 外部声明,供其他文件调用
extern const CN_FontDef CN_Font_16x16;
结构体设计:将字库的核心属性(宽 / 高 / 数据 / 数量)封装,便于扩展(比如后续添加 24×24 字体时,只需新增结构体);
使用声明:添加字体要实时修改.cn_count = n(实际字库数量)
3. 点阵数据查询函数:Font_GetCNData
const uint8_t* Font_GetCNData(const CN_FontDef *font, uint16_t index)
{
// 空指针 + 索引范围双重校验
if (font == 0 || font->cn_data == 0 || index >= font->cn_count) {
return 0;
}
// 每个字符占32字节,计算起始地址
return &font->cn_data[index * 32];
}
参数校验:font == 0:避免传入空指针导致程序崩溃;
index >= font->cn_count:避免索引超出字符数量(比如索引 2 时,字符数量仅 2,直接返回 NULL);
地址计算:每个 16×16 字符占 32 字节,因此第index个字符的起始地址 = 字库起始地址 + index × 32;
示例:index=0 → 指向 “你” 的起始地址(&cn_font_16x16_data[0]);index=1 → 指向 “好” 的起始地址(&cn_font_16x16_data[32]);
4. 图片点阵数据定义:cn_font_16x16_data
const unsigned char gImage_1[134400] = { /* 0X10,0X10,0X00,0XF0,0X01,0X18,0X01,0X1B, */
0X10,0XA4,0X10,0XA4,0X10,0XA4,0X10,0XA4,0X10,0XA4,0X10,0XA4,0X10,0XA4,0X10,0XA4,
0X10,0XA4,0X10,0XA4,0X10,0XA4,0X18,0XC4,0X18,0XC4,0X18,0XC4,0X18,0XC4,0X18,0XC4,
....}
数据格式:240×280分辨率的图像占用空间=240×280×2(RGB565 16位两字节)≈134KB
3.字模软件ATK_XFONT.exe使用配置
1.点击中间齿轮设置图标设置如下:

2.设置属性参数

通过属性调试,找到适合自己样式,没有问题点击生成字模,复制字模数据到fonts.c
static const uint8_t cn_font_16x16_data[] = {
0x08,0x80,0x08,0x80,0x08,0x80,0x11,0xFE,0x11,0x02,0x32,0x04,0x34,0x20,0x50,0x20,
0x91,0x28,0x11,0x24,0x12,0x24,0x12,0x22,0x14,0x22,0x10,0x20,0x10,0xA0,0x10,0x40,//你0
0x10,0x00,0x10,0xFC,0x10,0x04,0x10,0x08,0xFC,0x10,0x24,0x20,0x24,0x20,0x25,0xFE,
0x24,0x20,0x48,0x20,0x28,0x20,0x10,0x20,0x28,0x20,0x44,0x20,0x84,0xA0,0x00,0x40,//好1
//自定义添加
};同时更新结构体中字体数量.cn_count =2(实际字库数量)
4.图片取模软件 Img2Lcd.exe使用配置
1.参数配置设置要求:
RGB565➡16位真彩色
像素大小240×280
注意事项:取消勾选 包含图像头数据;
勾选 字节顺序Big Endian(大端模式):高字节在前,低字节在后。
输出图像选择: 勾选 R:5bits,G:6bits,B:5bits 16位彩色
2.图片裁剪
自定义图片使用画图(window自带软件打开,完成自定义裁剪

完成设置后修改为屏幕分辨率大小240×280,另存图像在Img2Lcd打开

保证图片数据输入像素(240.280)输出像素(240.280)之后,点击保存在记事本/VScode打开;复制到fonts.c中
const unsigned char gImage_1[134400] = { /* 0X10,0X10,0X00,0XF0,0X01,0X18,0X01,0X1B, */
0X10,0XA4,0X10,0XA4,0X10,0XA4,0X10,0XA4,0X10,0XA4,0X10,0XA4,0X10,0XA4,0X10,0XA4,
0X10...}.在fonts.h声明 extern const unsigned char gImage_1[134400];
在调用时ST7789_DrawImage(0, 0, 240, 280, gImage_1);
5. main.c测试实现
在st7789驱动 V1.0中测试屏幕填充、字符、绘制图案,这里不再测试相关测试函数ST7789_Test:
void ST7789_Test(void)
{
//中文字体测试
//单个中文字体,红字黑底
ST7789_DrawCNChar_16x16(10, 60, 0, &CN_Font_16x16, RED, BLACK);
//连续中文,绿字白底
uint16_t cn_index[] = {0,1}; // 你(0)、好(1)
ST7789_DrawCNString_16x16(10, 40 ,cn_index, 2, &CN_Font_16x16, GREEN, WHITE);
//图片显示测试
HAL_Delay(3000);
ST7789_Fill_Color(WHITE);
ST7789_DrawImage(0, 0, 240, 280, gImage_1);
}
在main.c中调试调用与上文st7789驱动 V1.0中保存不变。
三、烧录测试验证
效果示例:


PS:如果编译过程中很多报错
Error: L6406E: No space in execution regions with .ANY selector matching xxx.o(.text).,或者Error: L6220E: Execution region ER_IROM1 (size 0x80000 with 0x7F800 used) is full.等内存有关报错,有可能你图片显示太多,flash空间不管导致编译报错,根据自己板子flash大小选择。
总结
开启DMA提高刷屏效率,中文字体和图片显示为您的项目提供可视化
后续持续更新STM32,ESP32相关外设,欢迎有问题一起探讨交流.
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)