ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F103驱动ST7789:从SPI初始化到DMA刷屏的完整指南

STM32F103驱动ST7789:从SPI初始化到DMA刷屏的完整指南 简介这是一份面向嵌入式开发者的ST7789 LCD驱动与STM32F103接线示例资源聚焦小尺寸TFT屏幕在STM32平台的移植与SPI通信实现适合刚接触屏幕驱动或需要快速搭建显示工程的单片机学习者。压缩包共4个文件主要包含驱动源文件.c/.h及2个备份文件整体仅1KB属于轻量级代码片段便于直接查看移植要点或对照初始化流程。目前已有365人学习说明该驱动片段对同类型项目具备一定参考价值。借助其中代码读者可以了解ST7789的初始化序列、显示区域设置、颜色模式配置等关键步骤并结合STM32F103的SPI外设进行数据交互与屏幕点亮。由于文件精简适合作为入门调试模板也便于在现有工程中快速集成或二次修改。1. 为什么 STM32F103 点亮 ST7789 这件事看起来简单却总在同一个地方翻车标题里的 st7789.zip 十有八九是从网盘、论坛或某个分享站里拿到的驱动例程包解压后通常是一份 STM32F103 工程、一段初始化数组、几个延时循环、一个画点函数。屏幕不贵、单片机也不贵ST7789 这颗驱动 IC 在 1.3 寸到 2.4 寸 IPS 屏上遍地都是配一块 STM32F103 最小系统板二十元以内就能得到一块能跑彩色 UI 的显示核心。但实际点亮过程中十个坑里八个不在面板本身而在 SPI 时序极性、复位后的等待时间、以及 ST7789 特有的 GRAM 窗口设置。这三处错了表现又非常相似——要么白屏要么花屏很多人把三份代码拼来拼去仍然无解。所以先别急着拷那段初始化数组把 ST7789 当成一颗独立 IC 而不是一块屏幕来理解后面所有命令和延时才有据可依。2. ST7789 初始化序列与 GRAM 寻址哪些命令不能省、延时怎么给2.1 先分清 ST7789 和 ILI9341、ST7735 的差别ST7789V 是矽创电子出品的 TFT LCD 驱动控制器最大支持 240×320在 240×240 的 1.3 寸、1.54 寸屏上几乎是默认答案。和 ILI9341 最大的区别在于默认扫描方向ILI9341 上电后偏横屏ST7789 上电后偏竖屏而很多 240×240 模组的偏光片方向是 90 度贴装于是你常看到有人抱怨画面上下颠倒。这不是初始化写错了是 MADCTL 没按屏厂方向重写。和 ST7735 相比ST7789 在 RGB565 之外还支持 RGB666 的 18bit 色深但走 SPI 的常规用法大多只用到 16bit这个差异在纯色过渡带或渐变背景上肉眼可见平时不值得为它多花带宽。这套驱动和 ST7735 的另一处不同在窗口寻址。ST7789 的 CASET/RASET 命令分别设置列范围和行范围两个命令都是 16 位参数、高字节在前。后续写入像素数据时内部地址会在窗口内自动步进步进顺序由 MADCTL 的 MY/MX 位决定。这意味着你可以只刷屏幕的一个矩形区域比如只更新时钟数字所在的小块而不是每次整屏全推。这个特性在后面做局部刷新优化时是关键基础。2.2 初始化命令时序表哪几条动不了哪几条可以抄作业网上流传的 ST7789 初始化序列版本很多但主线基本一致。我先给一张“最小组”命令表后面代码里的初始化数组就是按这张表展开的命令名称参数示例最小延时作用0x01SWRESET无5ms软件复位效果等同拉 RES0x11SLPOUT无120ms退出睡眠等振荡器稳定0x36MADCTL0x000扫描方向与 RGB/BGR 顺序0x3ACOLMOD0x050像素格式0x05 为 16bit0x21INVON无0反转显示多数模组需要0x29DISPON无1ms打开显示最后执行0x01 和 0x11 后面的延时是整套初始化里最容易被人压缩的地方。ST7789 数据手册里写明电源稳定和内部振荡器起振都需要时间0x11 后给不足 120ms 的典型后果是第一次上电白屏拔电重来又正常显得非常“玄学”。其实只是 RC 充电时间不够把延时补足就稳定了。0x21 这个反转命令很多人会漏掉因为部分模组出厂偏光片方向不同不加的话白色区域会带底色彩色还容易偏暗。2.3 初始化数组用结构体表比一长串啰嗦命令更合理这里给一份我在 F103 上调通的初始化数组以结构体表形式组织。它的好处是新增一块屏时只改数组、不碰驱动逻辑坏处是新手改参数时容易只看数值不看前后命令。这个数组里的伽马、电压值沿用了一套社区通用的 ST7789V 配置不同屏厂会微调先照搬能亮再按屏厂标注的参考手册数值替换。typedef struct { uint8_t cmd; /* 命令字 */ uint8_t len; /* 后续参数字节数 */ uint8_t data[8]; /* 参数值 */ } st7789_seq_t; static const st7789_seq_t st7789_init_tbl[] { {0x01, 0, {0}}, /* SWRESET 软复位 */ {0x11, 0, {0}}, /* SLPOUT 退出睡眠 */ {0x36, 1, {0x00}}, /* MADCTL RGB默认方向 */ {0x3A, 1, {0x05}}, /* COLMOD 16bit RGB565 */ {0xB2, 5, {0x0C, 0x0C, 0x00, 0x33, 0x33}}, /* Porch 控制 */ {0xB7, 1, {0x35}}, /* Gate 控制 */ {0xBB, 1, {0x19}}, /* VCOM 设置 */ {0xC0, 1, {0x2C}}, /* LCM 控制 */ {0xC2, 1, {0x01}}, /* VDV/VRH 命令使能 */ {0xC3, 1, {0x12}}, /* VRH 设置 */ {0xC4, 1, {0x20}}, /* VDV 设置 */ {0xC6, 1, {0x0F}}, /* 帧率控制 */ {0xD0, 2, {0xA4, 0xA1}}, /* 电源控制 1 */ {0xE0, 14, {0xD0,0x04,0x0D,0x11,0x13,0x2B,0x3F,0x54,0x4C,0x18,0x0D,0x0B,0x1F,0x23}}, /* 正伽马 */ {0xE1, 14, {0xD0,0x04,0x0C,0x11,0x13,0x2C,0x3F,0x44,0x51,0x2F,0x1F,0x1F,0x20,0x23}}, /* 负伽马 */ {0x21, 0, {0}}, /* INVON 反色开启 */ {0x29, 0, {0}}, /* DISPON 显示开启 */ };这段数组里有几个点要留意。0x3A 的值 0x05 对应 16bit RGB5650x06 才是 RGB666写错会出现颜色带宽不足的渐变断层。0x36 的 bit7、bit6、bit5 分别控制 MY、MX、MV也就是行方向、列方向、页列交换旋转屏幕本质就是改这三个位。0xB2 到 0xD0 这一段属于电源和栅极时序参数不同屏厂给出的推荐值不一样多数情况下照抄这份能正常显示但如果出现亮度不均、顶端横纹优先动 0xBB 的 VCOM 和 0xC3 的 VRH。2.4 中文参考手册怎么读别被 0x36 的位定义带偏现在搜 st7789 驱动中文参考手册能找到的中文版大多是从英文数据手册翻译而来版本新旧不一。最常出错的位定义在 0x36 MADCTL 上有些翻译把 bit3BGR和 bit2MH的说明弄反导致新手按手册去调 RGB 顺序调了半天颜色反而更怪。稳妥做法是只信任英文原版里的 bit 表格中文版只用来对照命令名称和大致功能。0x36 一共 8 个 bit实际常用只有 MY、MX、MV、BGR 这四个分别对应 0x80、0x40、0x20、0x08。记不清时就把这四个值画在纸上按二进制位拼出目标方向比硬背 0x00、0xC0、0xA0 这些组合可靠。3. STM32F103 最小系统接 ST7789SPI1 引脚分配、底层读写函数和初始化调用3.1 接线表与跑通原则STM32F103 的 SPI1 在 PA5SCK、PA6MISO、PA7MOSI48 脚、64 脚、100 脚封装都引出了所以用 SPI1 而不是 SPI2 可以省去查找重映射的麻烦。ST7789 的 SDO 脚在只写模式下可以不接MISO 留空天然避开和串口、JTAG 的引脚冲突。我常用的最小系统接线是下面这张表ST7789 引脚F103 引脚说明VCC3.3V逻辑电压最高 3.3V别接 5VGNDGND必须共地SCLPA5SPI1 时钟SDAPA7SPI1 主机输出RESPA3复位低电平复位DCPA4命令/数据选择CSPA2片选低有效BLK3.3V 串 10Ω背光部分模块需要限流PA2、PA3 同时是 USART2 的 TX/RX但这里只做普通推挽输出不使能 USART2 就不会冲突。PA4 在 SPI1 里的默认功能是硬件 NSS驱动里不用硬件片选软件拉高拉低更灵活。把 CS、DC、RES 放在这三个脚上还有一个好处是从网上找的 F103 最小系统原理图导出引脚后不需要飞线就能直接烧录调试。上电前先量一下 VCC 和 GND 是否短路很多拆机屏模块的排线焊接点非常靠近这一步能省下半小时排查时间。3.2 标准外设库 V3.50 下的 SPI1 初始化现在搜 stm32f103 库 v3.50 下载下载量最大的基本是标准外设库 3.5.0。这套库虽然老但仍然是大量 F103 工程的默认底座函数命名和寄存器操作都能对上。SPI1 初始化代码如下void st7789_spi_init(void) { GPIO_InitTypeDef gpio; SPI_InitTypeDef spi; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); /* SCK、MOSI 复用推挽输出 */ gpio.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); /* CS、DC、RES 普通推挽输出 */ gpio.GPIO_Pin GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4; gpio.GPIO_Mode GPIO_Mode_Out_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); GPIO_SetBits(GPIOA, GPIO_Pin_2 | GPIO_Pin_3); /* CS、RES 默认高 */ spi.SPI_Direction SPI_Direction_1Line_Tx; spi.SPI_Mode SPI_Mode_Master; spi.SPI_DataSize SPI_DataSize_8b; spi.SPI_CPOL SPI_CPOL_Low; spi.SPI_CPHA SPI_CPHA_1Edge; spi.SPI_NSS SPI_NSS_Soft; spi.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; /* 72MHz / 4 18MHz */ spi.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, spi); SPI_Cmd(SPI1, ENABLE); }几个容易写错的参数说明SPI_Direction_1Line_Tx 表示只发送不接收ST7789 在这个模式下的数据手册时序是模式 0也就是 CPOL 低、CPHA 第一个边沿采样。CPOL 或 CPHA 写反后屏幕往往能点亮但颜色错乱而且是有规律的花色很难一眼定位到 SPI 配置。分频系数取 2 也就是 36MHz 时部分 ST7789 模组在长排线下会出现波形振铃导致偶发性花屏。18MHz 是 F103 和 ST7789 都绝对安全的档位刷新率足够不必追求极限。3.3 底层写命令、写数据函数重点是 CS 与 DC 的配合写命令和写数据的本质区别只在 DC 引脚电平DC 为 0 表示当前字节是命令DC 为 1 表示当前字节是数据。但实际工程里不能只写一个字节就切换因为一次命令往往跟着若干个参数CS 在这整段时间内都要保持低电平。下面这段封装了「命令 数据」的完整事务static void spi_tx_byte(uint8_t byte) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); SPI_I2S_SendData(SPI1, byte); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) SET); } void st7789_write_cmd_block(uint8_t cmd, uint8_t *data, uint8_t len) { GPIO_ResetBits(GPIOA, GPIO_Pin_2); /* CS 拉低事务开始 */ GPIO_ResetBits(GPIOA, GPIO_Pin_4); /* DC 0命令阶段 */ spi_tx_byte(cmd); if (len) { GPIO_SetBits(GPIOA, GPIO_Pin_4); /* DC 1数据阶段 */ while (len--) { spi_tx_byte(*data); } } GPIO_SetBits(GPIOA, GPIO_Pin_2); /* CS 拉高事务结束 */ }这段代码的关键在最后两个等待BSY 标志必须在 CS 拉高之前处理完否则最后一个字节还在移位寄存器里没送出去就被 CS 上升沿截断了。CS 在整个命令加参数期间保持低是因为 ST7789 在 CS 高电平时会认为当前传输结束如果每个参数之间都拉高 CS部分模组会丢失字节边界表现出随机性的画面错位。把 CS 的拉低、拉高收敛到一个函数里后续所有调用点都不会犯错。3.4 初始化与窗口设置调用前面封装好的函数初始化入口按第 2 章的数组逐条执行即可。需要注意的只有复位脉冲和睡眠退出的延时void st7789_init(void) { uint32_t i; GPIO_ResetBits(GPIOA, GPIO_Pin_3); /* RES 拉低开始复位 */ st7789_delay_ms(20); GPIO_SetBits(GPIOA, GPIO_Pin_3); /* RES 拉高复位结束 */ st7789_delay_ms(120); for (i 0; i sizeof(st7789_init_tbl) / sizeof(st7789_seq_t); i) { st7789_write_cmd_block( st7789_init_tbl[i].cmd, (uint8_t *)st7789_init_tbl[i].data, st7789_init_tbl[i].len); if (st7789_init_tbl[i].cmd 0x01) { st7789_delay_ms(5); } else if (st7789_init_tbl[i].cmd 0x11) { st7789_delay_ms(120); } } } void st7789_set_window(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { uint8_t tmp; tmp x0 8; st7789_write_cmd_block(0x2A, tmp, 1); tmp x0 0xFF; st7789_write_cmd_block(0x2A, tmp, 1); tmp x1 8; st7789_write_cmd_block(0x2A, tmp, 1); tmp x1 0xFF; st7789_write_cmd_block(0x2A, tmp, 1); tmp y0 8; st7789_write_cmd_block(0x2B, tmp, 1); tmp y0 0xFF; st7789_write_cmd_block(0x2B, tmp, 1); tmp y1 8; st7789_write_cmd_block(0x2B, tmp, 1); tmp y1 0xFF; st7789_write_cmd_block(0x2B, tmp, 1); }st7789_set_window 这里的写法故意用一条命令写一个字节目的是让命令块函数在每次调用时都自动处理 CS逻辑简单但效率不高。实际项目中可以改成先发 0x2A、再连发四个参数效率更高。0x2A 是列地址0x2B 是行地址0x2C 是开始写显存。窗口设置后每次写入像素数据都从窗口左上角开始按行列步进填充填满窗口后自动停止这是后续局部刷新的基础。4. STM32F103 刷 240×240 全屏的带宽账轮询转 DMA行缓冲怎么设计4.1 轮询方式刷全屏为什么主循环直接被拖死240×240 分辨率、RGB565 格式一帧完整数据量是 240×240×2 115200 字节。SPI1 在 18MHz 时钟下理论每字节 8 个时钟周期大概 0.44 微秒算下来一帧理论极限约 50 毫秒。实际轮询代码里每次 TXE 检查、SendData、BSY 等待都会有开销一个字节实际耗时约 0.7 到 1 微秒整帧刷完约 80 到 100 毫秒。这个速度带来的 fps 只有 10 左右且 CPU 全部耗在等待标志位上连按键扫描都会变得迟钝。很多人以为换更高的 SPI 分频就能解决但 36MHz 下波形质量变差长线传输容易偶发丢字节反而不如守住 18MHz 然后用 DMA 把等待时间藏起来。DMA 的收益并不是压缩带宽而是让 CPU 不参与每个字节的搬运传输期间可以继续渲染下一行画面或者处理串口数据。4.2 DMA1 通道 3 的配置代码F103 的 DMA1 通道 3 固定对应 SPI1_TX这个映射不需要查重映射表直接配置即可。初始化代码uint8_t g_fb[240 * 2 * 16]; /* 16 行 x 240 列 x 2 字节的行缓冲 */ void st7789_dma_init(void) { DMA_InitTypeDef dma; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); dma.DMA_PeripheralBaseAddr (uint32_t)SPI1-DR; dma.DMA_MemoryBaseAddr (uint32_t)g_fb; dma.DMA_DIR DMA_DIR_PeripheralDST; dma.DMA_BufferSize 0; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; dma.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; dma.DMA_Mode DMA_Mode_Normal; dma.DMA_Priority DMA_Priority_High; dma.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel3, dma); DMA_Cmd(DMA1_Channel3, DISABLE); }外设地址写成 SPI1-DR 而不是某个固定的数据地址是因为 SPI 数据寄存器每写入一次就产生一个发送请求DMA 的搬运目标就是这个寄存器。MemoryInc 必须使能否则所有字节都会搬到同一个内存地址PeripheralInc 必须禁止否则 DMA 会尝试递增 SPI1 的寄存器地址。BufferSize 在 DMA_Init 时写 0 没关系实际长度在每次刷新时用 DMA_SetCurrDataCounter 设置。4.3 全屏刷新函数窗口之后直接交给 DMADMA 推屏的完整流程是先设窗口再拉低 CS、拉高 DC然后启动 DMA 和 SPI 的 DMA 请求等传输完成后再拉高 CS。关键点在于开启顺序void st7789_refresh(uint8_t *buf, uint32_t len) { st7789_set_window(0, 0, 239, 239); GPIO_ResetBits(GPIOA, GPIO_Pin_2); /* CS 拉低 */ GPIO_SetBits(GPIOA, GPIO_Pin_4); /* DC 1数据模式 */ DMA_Cmd(DMA1_Channel3, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel3, len); DMA1-IFCR DMA1_FLAG_TC3 | DMA1_FLAG_HT3 | DMA1_FLAG_TE3; DMA_Cmd(DMA1_Channel3, ENABLE); SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE); while (DMA_GetFlagStatus(DMA1_FLAG_TC3) RESET); DMA_ClearFlag(DMA1_FLAG_TC3); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) SET); GPIO_SetBits(GPIOA, GPIO_Pin_2); /* CS 拉高 */ }先 DMA_Cmd 使能 DMA再 SPI_I2S_DMACmd 使能 SPI 的发送请求这个顺序反过来的话SPI 可能先触发一次空请求DMA 还没准备好导致首字节污染。DMA 传输过程中不要碰 CS 和 DC 电平等 BSY 清掉后再拉高 CS否则最后一拍数据会被截断。这个函数里用的 g_fb 是第 4.2 节定义的行缓冲实际使用时应传入需要刷新的数据地址。4.4 F103 的内存到底放不放得下双缓冲先算一笔账240×240×2 等于 115200 字节即 112.5KB。而 STM32F103C8T6 只有 20KB SRAMF103ZET6 是 64KB全屏双缓冲是完全不现实的。所以网上那些定义uint16_t framebuffer[240][240]的例程在 F103 上连编译都不该通过如果编译过了运行到一半也会栈溢出或变量被踩表现成花屏死机且问题不在屏而在内存。工程里常见做法是两个方向一是开一个 16 行的条带缓冲数据量 240×16×2 7680 字节C8T6 能轻松装下画完一段就调用 st7789_refresh 推一段二是只留一行缓冲 480 字节配合画图函数逐行产出像素。第二种省内存但慢适合界面变化不频繁的场景。如果你要在 stm32f103 上同时做多路捕获然后把结果画成波形建议用条带缓冲把捕获数据写进条带、DMA 把条带推上屏CPU 在等待 DMA 完成时还能继续处理捕获中断。5. ST7789 排错顺序白屏、方向、以及和 FreeRTOS 共用串口调试的配合5.1 白屏排查的三个检查点白屏是 ST7789 最常见的症状按以下顺序排查能覆盖九成原因。第一检查 RES 复位脉冲宽度是否足够ST7789 要求低电平至少 10 微秒工程里用 20 毫秒按理足够但部分模组上电瞬间电源还没稳定复位就白做了建议把上电到复位的间隔拉长到 50 毫秒。第二确认 0x11 SLPOUT 后的延时真的执行了 120 毫秒很多例程在 while 循环里被编译优化或死在某个中断里延时函数实际没跑够。第三量背光引脚BLK 不接或虚焊时屏其实已经点亮但背光不亮看起来就是白屏。排到这三项之后再用示波器或逻辑分析仪看 PA7 上有没有波形如果第一个字节就是乱的回头看 SPI 的 CPOL/CPHA 和 DC 引脚是否接反。5.2 MADCTL 方向与 RGB 顺序的快速修正画面上下或左右颠倒时不需要改代码逻辑只改 0x36 的参数。下面这张方向表以 0x00 为左上角起点目标方向MADCTL 参数组合位正常方向0x00MY0 MX0 MV0右旋 90 度0x60MY0 MX1 MV1上下颠倒 180 度0xC0MY1 MX1 MV0左旋 90 度0xA0MY1 MX0 MV1如果颜色整体偏蓝或偏红把 0x36 的 bit3 也就是 0x08 翻转0x00 与 0x08 互切。这里注意有些模块不只是偏色而是蓝色和红色互换这就是 BGR 位反了的直接证据。调方向时用一个纯色矩形加一个字符图案最直观纯色只能确认 RGB 顺序字符才能确认上下左右。5.3 把 ST7789 驱动挂进 FreeRTOS 的注意点以及串口调试的选用如果你正在做 FreeRTOS 移植 stm32f103 并让显示任务在后台跑DMA 完成回调里不能直接调用 st7789_refresh。那个函数本身要阻塞等待 DMA 完成在中断上下文里调用会进入死循环。正确做法是 DMA 中断里给一个信号量显示任务阻塞在信号量上等发送完成。这样 SPI 总线不会长期被独占显示任务优先级可以比串口任务低。调试串口的选择上串口 1 和串口 3 的差异在实际使用时很值得注意USART1 挂在 APB2 上最高 72MHzUSART3 挂在 APB1 上最高 36MHz。同样的波特率分频配置串口 1 的容错更好打印日志不容易乱码所以刷屏调试时优先把日志放到串口 1。串口 3 留给和 485 收发这类外设通信避免调试信息和生产数据混在一起。另外如果 DAP 下载失败、报连接不上先检查最小系统板上 BOOT0 和 BOOT1 是否都拉低这两个引脚悬空时调试器复位时序会被干扰和屏幕本身没有关系别在驱动里浪费排查时间。使用串口 1 打印 ST7789 的初始化完成标志再用一块固定的红绿蓝测试图刷屏确认 DMA 链路正常。本文还有配套的精品资源点击获取
返回列表