STM32驱动SPI彩屏:从硬件连接到DMA刷屏的完整实战指南

📅 2026/7/29 8:34:44 👁️ 阅读次数 📝 编程学习
STM32驱动SPI彩屏:从硬件连接到DMA刷屏的完整实战指南

1. 从点灯到点亮世界:为什么SPI彩屏是STM32入门的绝佳跳板

很多朋友学STM32,都是从点亮一个LED开始的。这当然没错,但点亮LED之后呢?当你能熟练地翻转GPIO口,配置好定时器,甚至玩转了串口通信,下一步的成就感从哪里来?我的经验是,驱动一块彩屏。这不仅仅是让一个像素亮起来,而是让一个世界亮起来。你能在屏幕上画出线条,显示文字,甚至播放动画,这种从“控制”到“呈现”的飞跃,带来的满足感是指数级的。而SPI接口的IPS彩屏,正是实现这个飞跃最平滑、最友好的阶梯。

为什么是SPI?为什么是IPS?对于初学者而言,并行总线驱动的屏幕(比如8080或RGB接口)虽然速度快,但动辄需要占用十几个甚至几十个GPIO引脚,接线复杂,配置繁琐,很容易在硬件连接上就踩坑。而SPI(Serial Peripheral Interface)是一种高速、全双工、同步的串行通信总线,通常只需要4根线(SCK时钟、MOSI主机输出、MISO主机输入、CS片选)就能完成数据交换。对于驱动一个分辨率不高的彩屏(比如常见的1.14寸、1.3寸、1.54寸屏),SPI的速率完全够用,极大地简化了硬件连接和软件驱动的工作量。

IPS(In-Plane Switching)则是一种屏幕显示技术,相比老旧的TN屏,它具有可视角度广、色彩还原好、亮度均匀等优点。现在市面上主流的低成本小尺寸彩屏,几乎都是IPS面板。所以,“STM32驱动IPS彩屏”这个组合,几乎成了单片机玩家从入门迈向实战的“必修课”和“标志性项目”。它综合考察了你对GPIO、时钟、SPI外设的理解,以及对特定器件数据手册的阅读能力和底层通信协议的实现能力。接下来,我将以一个典型的1.54英寸、分辨率为240x240的SPI IPS彩屏(驱动芯片常为ST7789或ST7735S)为例,手把手带你走通整个流程,并分享那些数据手册里不会写的“坑”和技巧。

2. 硬件连接与驱动芯片选型:不只是接对线那么简单

在写第一行代码之前,我们必须把硬件搞清楚。一块SPI彩屏模块,其核心通常是一块液晶面板和一颗驱动芯片。驱动芯片负责接收来自MCU的指令和数据,并转换成控制液晶分子偏转的信号。我们编程的对象,其实就是这颗驱动芯片。

2.1 驱动芯片揭秘:ST7789VW的通信逻辑

目前最流行的驱动芯片是ST7789V(用于240x240屏)和ST7735S(用于160x128等屏)。我们以ST7789V为例。你可以把它想象成一个有“内存”的“翻译官”。MCU通过SPI总线发送两种东西:命令(Command)和数据(Data)。命令用来设置芯片的工作模式,比如屏幕扫描方向、颜色格式、睡眠唤醒等;数据则是要显示在屏幕上的具体像素颜色值。

这里有一个关键概念:DC(Data/Command)引脚,有时也叫RS或A0引脚。这个引脚的高低电平,决定了你当前通过SPI发送的字节是命令还是数据。通常,DC=0表示发送的是命令(CMD),DC=1表示发送的是数据(DATA)。这是SPI驱动彩屏与驱动普通SPI器件(如Flash、传感器)最大的不同之处,多了一根控制线。

因此,完整的硬件连接通常需要6根线:

  • SCK (SPI时钟线): 连接到STM32的SPI_SCK引脚。
  • MOSI (主机输出从机输入): 连接到STM32的SPI_MOSI引脚,用于发送数据到屏幕。
  • DC (数据/命令选择): 连接到任意一个GPIO输出引脚。
  • CS (片选): 连接到任意一个GPIO输出引脚。虽然SPI协议支持多设备,但这里我们通常只接一块屏,理论上可以硬件拉低(接地)永远使能。但为了代码规范和省电,建议还是用GPIO控制。
  • RESET (复位): 连接到任意一个GPIO输出引脚。用于硬件复位屏幕驱动芯片,这是一个非常重要的可靠性设计。
  • BACKLIGHT (背光控制): 连接到任意一个GPIO输出引脚(或直接接VCC常亮)。用于控制屏幕背光开关。

注意:有些模块为了省引脚,会取消独立的RESET引脚,或者将背光控制做成固定上拉。务必以你手中模块的实际原理图或卖家资料为准。有RESET引脚会大大增加初始化的成功率。

2.2 STM32引脚配置:硬件SPI vs 软件模拟SPI

接下来是STM32侧的配置。你需要为SPI外设和几个GPIO分配具体的引脚。

方案一:使用硬件SPI(推荐)这是最标准、效率最高的方式。你需要查阅STM32的芯片数据手册(Datasheet)和引脚复用功能表(Alternate Function),找到标有SPI1_MOSI、SPI1_SCK等功能的引脚。例如,在STM32F103C8T6(蓝桥杯常用芯片)上:

  • SPI1_SCK -> PA5
  • SPI1_MOSI -> PA7
  • SPI1_MISO -> PA6(驱动屏幕只发不收,此引脚可不接)
  • SPI1_NSS -> PA4(硬件片选,但通常我们用软件控制GPIO,此引脚也可不用)

然后,DC、RESET、CS、BL引脚可以任意分配方便布板的GPIO,如PB0、PB1、PB10、PB11等。

使用硬件SPI的好处是,数据发送由DMA或SPI外设自动完成,不占用CPU时间,速率稳定且快。

方案二:使用软件模拟SPI(GPIO模拟时序)如果你的硬件SPI引脚被其他功能占用,或者想彻底理解SPI时序,可以用任意GPIO口来模拟。你需要用代码控制SCK和MOSI引脚的高低电平变化,来模拟出时钟和数据信号。这种方式灵活,但速度慢,CPU占用率高,适合学习原理或引脚资源极度紧张的情况。

对于初学者,我强烈建议先从硬件SPI开始。它能让你更专注于驱动逻辑本身,而不是纠结于微秒级的延时是否精准。在CubeMX中配置硬件SPI是非常直观的。

3. 软件驱动层构建:从初始化到画出第一个点

硬件连接无误后,我们进入软件部分。驱动代码可以抽象为三个层次:底层硬件接口层、驱动芯片命令层、上层应用图形层。我们先从最底层开始。

3.1 SPI与GPIO的底层接口函数

无论你用HAL库、标准库还是LL库,都需要实现几个最基本的函数:

  1. 写命令函数LCD_Write_Cmd(uint8_t cmd):

    • 拉低DC引脚(表示接下来发送的是命令)。
    • 拉低CS引脚(选中设备)。
    • 通过SPI发送一个字节cmd
    • 拉高CS引脚(释放设备)。
  2. 写数据函数LCD_Write_Data(uint8_t data):

    • 拉高DC引脚(表示接下来发送的是数据)。
    • 拉低CS引脚。
    • 通过SPI发送一个字节data
    • 拉高CS引脚。
  3. 写数据流函数LCD_Write_Data_Buffer(uint8_t *buffer, uint32_t length):

    • 这是优化性能的关键。当需要连续写入大量像素数据时(比如刷屏),应该使用SPI的连续发送功能或DMA,而不是循环调用单字节发送函数。在HAL库中,可以使用HAL_SPI_Transmit(&hspi1, buffer, length, HAL_MAX_DELAY)
  4. 复位函数LCD_Reset(void):

    • 拉低RESET引脚,保持至少10ms(具体看芯片手册,通常10ms足够)。
    • 拉高RESET引脚,再延时至少120ms,等待芯片内部稳定。

这些函数是你的“武器”,后续所有高级操作都建立在它们之上。这里有一个重要技巧:在LCD_Write_CmdLCD_Write_Data中,发送完数据后,最好等待SPI发送完成标志(如HAL库的HAL_SPI_GetState或检查BSY标志),再进行下一步操作或切换DC引脚电平。虽然大部分时间没问题,但在高速SPI下,不等待完成就操作DC引脚,可能导致时序错乱。

3.2 驱动芯片初始化序列:照抄还是理解?

网上有很多驱动ST7789的代码,初始化部分往往是一长串固定的命令和数据。直接复制粘贴可能也能点亮屏幕,但一旦出问题就会毫无头绪。我建议你至少理解其中关键几条命令的含义

初始化序列的本质,就是按照驱动芯片数据手册(Datasheet)要求的顺序和参数,配置其内部寄存器。以ST7789V为例,关键的初始化步骤包括:

  1. 软件复位(SWRESET):发送命令0x01。这是软件复位,配合硬件复位使用,确保芯片状态已知。
  2. 退出睡眠模式(SLPOUT):发送命令0x11,然后需要等待120ms以上。芯片上电后默认在睡眠模式以省电,必须唤醒它。
  3. 设置颜色接口格式(COLMOD):发送命令0x3A,后面跟一个数据字节,指定颜色深度。常见的是0x55(16位RGB565格式)或0x66(18位RGB666格式)。RGB565是我们最常用的格式,它用一个16位(2字节)整数表示一个像素:高5位是红色,中间6位是绿色,低5位是蓝色。这决定了你后续发送像素数据的方式。
  4. 设置内存数据访问控制(MADCTL):发送命令0x36,后面跟一个数据字节。这是控制屏幕旋转和镜像的核心命令!这个字节的每一个bit都有含义:
    • Bit 7: MY,行地址顺序(上下镜像)
    • Bit 6: MX,列地址顺序(左右镜像)
    • Bit 5: MV,行列交换(横屏/竖屏)
    • Bit 4: ML,垂直刷新顺序
    • Bit 3: RGB,RGB/BGR顺序(颜色分量顺序,非常重要!)
    • Bit 2: MH,水平刷新顺序 例如,0x00表示正常竖屏模式;0xA00xC0可能用于横屏模式。如果发现屏幕颜色不对(比如红色和蓝色反了),大概率是这里的RGB/BGR位设置错了。
  5. 设置列地址和行地址(CASET, RASET):命令0x2A和0x2B。用于设置接下来要写入的显存区域。全屏刷新时,设置为整个屏幕的范围(如X: 0~239, Y: 0~239)。
  6. 打开显示(DISPON):发送命令0x29。在这之前,屏幕可能是白屏或乱码,发送此命令后,图像才会真正显示出来。

网上找到的初始化代码,往往是一整个数组,包含了所有命令和等待延时。你需要做的是:1. 确保延时足够;2. 根据你的屏幕型号和连接方式,调整MADCTL和COLMOD等关键参数。很多屏幕初始化不成功,问题就出在唤醒延时不够,或者颜色格式、扫描方向设置错误。

3.3 核心绘图函数:SetWindow 与 DrawPixel

有了初始化,屏幕应该已经亮起(可能是白屏、花屏或某种纯色)。接下来要实现最基础的两个函数:设置窗口和画点。

设置窗口函数LCD_SetWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2): 这个函数告诉驱动芯片:“我接下来要写入的像素数据,是填充到从(x1,y1)到(x2,y2)的这个矩形区域里”。实现如下:

  1. 发送命令0x2A (CASET),然后依次发送x1和x2坐标的高8位和低8位(共4字节数据)。注意芯片可能要求先高字节后低字节。
  2. 发送命令0x2B (RASET),然后依次发送y1和y2坐标的高8位和低8位。
  3. 发送命令0x2C (RAMWR),告诉芯片“接下来是像素数据了”。之后,你就可以连续地向SPI总线写入像素数据,芯片会自动从(x1,y1)开始,从左到右、从上到下填充这个窗口。

画点函数LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color): 这是所有图形的基础。实现就是先设置一个1x1像素的窗口,然后写入一个颜色数据。

void LCD_DrawPixel(uint16_t x, uint16_t y, uint16_t color) { LCD_SetWindow(x, y, x, y); // 设置窗口为单个点 LCD_Write_Data(color >> 8); // 发送颜色高字节 (RGB565格式) LCD_Write_Data(color & 0xFF); // 发送颜色低字节 }

但请注意,这样画点的效率极低,因为每次画点都要重复发送设置窗口的命令。在实际的图形填充中,我们会先设置好一个大窗口,然后一次性写入所有像素数据。

4. 性能优化与高级功能:让画面流畅起来

当你用DrawPixel循环画出一个矩形或一条线后,可能会发现速度很慢,甚至有闪烁感。这是因为我们还没有充分利用SPI和驱动芯片的能力。

4.1 DMA:解放CPU的刷屏利器

刷屏(FillScreen)或显示一张图片,涉及海量的像素数据传输(240*240=57600个像素,RGB565下就是115200字节)。如果用循环调用LCD_Write_Data,CPU会被完全占用在等待SPI发送完成上。

此时,DMA(直接存储器访问)就是救星。DMA可以在不占用CPU的情况下,自动将内存中的数据搬运到SPI的数据寄存器中。配置步骤如下(以STM32CubeMX和HAL库为例):

  1. 在CubeMX中,为你使用的SPI外设(如SPI1)的TX通道启用DMA。
  2. 在代码中,定义一个足够大的缓冲区(例如uint16_t lcd_buffer[240*240]),用于存放一帧图像的所有像素数据(RGB565格式)。
  3. 当需要刷屏时,先用memset或循环将整个缓冲区填充为同一个颜色值。
  4. 调用LCD_SetWindow(0, 0, 239, 239)设置全屏窗口。
  5. 调用HAL_SPI_Transmit_DMA(&hspi1, (uint8_t*)lcd_buffer, 240*240*2)启动DMA传输。
  6. CPU此时就自由了,可以去处理其他任务(如按键扫描、传感器读取)。你可以等待DMA传输完成中断,或者简单地延时一小段时间。

使用DMA后,全屏刷新的速度会有质的提升,画面会变得非常流畅。这是实现动态显示、动画的基础。

4.2 显存与局部刷新:双缓冲的雏形

更高级的玩法是使用“显存”(Frame Buffer)。即,在MCU的内部RAM或外扩RAM中开辟一块和屏幕分辨率一样大的内存区域(uint16_t frame_buffer[240][240])。所有的绘图操作(画点、画线、画圆、显示字符)都只修改这个内存数组,而不直接操作屏幕。

当需要更新屏幕时,再将整个frame_buffer通过DMA一次性搬运到屏幕。这带来了两个巨大好处:

  1. 避免闪烁:直接操作屏幕,如果绘制复杂图形中间有延时,用户会看到绘制过程。而操作显存是瞬间完成的,一次性DMA刷屏,画面是整体更新。
  2. 局部刷新优化:你可以记录哪些区域的内容发生了改变(脏矩形),然后只刷新屏幕对应的那一部分窗口,而不是整个屏幕,从而进一步提高效率。

虽然对于STM32F1这类内存紧张的芯片,全分辨率双缓冲(两个frame_buffer)可能不现实,但单显存+局部刷新的思想是通用的,可以显著提升复杂图形界面的体验。

4.3 字库与图形界面基础

有了画点、画线(如Bresenham算法)、填充矩形等基本图形函数,就可以构建更上层的功能了。

显示字符:本质上就是画点。你需要一个字库数组。比如一个16x16像素的汉字,可以用一个长度为32的uint8_t数组表示(因为16*16/8=32,每个bit代表一个像素是否点亮)。显示时,遍历这个数组的每一个bit,如果为1,就在对应位置画前景色;如果为0,就画背景色(或跳过)。ASCII字符、中文字库都可以通过取模软件生成。

显示图片:将图片通过工具(如Img2Lcd)转换成C语言数组(RGB565格式)。显示时,设置好窗口,然后直接将这个数组通过DMA发送出去即可。

实操心得:在嵌入式显示中,颜色格式的统一至关重要。确保你的字库取模方式、图片转换格式、驱动芯片颜色模式(COLMOD)以及你代码中颜色常量的定义(如#define RED 0xF800)全部是RGB565格式。任何一环不匹配,都会导致颜色显示错误。

5. 调试与排坑指南:当屏幕不亮时该怎么办

驱动SPI彩屏的过程,几乎一定会遇到问题。下面是我总结的排查链路,你可以像侦探一样一步步检查。

现象一:屏幕完全无反应,背光都不亮。

  • 检查电源:万用表测量模块VCC和GND引脚电压是否为3.3V(或5V,根据模块要求)。背光引脚是否已供电或使能。
  • 检查复位:确保RESET引脚有正确的上电复位序列(先低后高)。可以用逻辑分析仪或示波器抓取波形,或者简单地在代码中给RESET引脚一个明确的“低-延时-高”操作,并确保延时足够(>10ms)。

现象二:背光亮,但屏幕是白屏、花屏或条纹。

  • 检查SPI时钟极性(CPOL)和相位(CPHA):也就是SPI的Mode。ST7789等驱动芯片通常工作在Mode 0(CPOL=0, CPHA=0) 或Mode 3(CPOL=1, CPHA=1)。这是最容易出错的地方!如果模式不对,数据根本读不进去。绝大多数屏幕模块默认是Mode 0。在CubeMX中配置SPI时,Clock Polarity选Low, Clock Phase选1 Edge。
  • 检查初始化序列和延时:用逻辑分析仪抓取SPI总线和DC、CS引脚的波形。对照数据手册的初始化时序图,看命令和数据是否在正确的时机(DC电平)发出。特别关注SLPOUT(0x11) 和DISPON(0x29) 命令后的延时是否足够。延时不够,芯片没准备好就进行下一步,会导致初始化失败。
  • 检查MADCTL参数:如果屏幕有显示但颜色怪异(红蓝反色)或方向不对,就是MADCTL寄存器的设置问题。重点检查RGB/BGR位(bit 3)和MV、MX、MY位。需要根据屏幕的物理安装方向和芯片数据手册来调整。一个笨办法是:尝试网上其他驱动代码里常见的几个MADCTL值(如0x00, 0xC0, 0xA0, 0x60),看哪个显示正常。

现象三:能显示,但刷新极慢,或局部乱码。

  • 检查SPI时钟频率:在CubeMX中,SPI的波特率预分频器不要设得太低。对于STM32F103,系统时钟72MHz,SPI1可以轻松跑到18MHz或36MHz。太低的时钟(如几百KHz)会导致刷屏极慢。
  • 检查数据发送函数:你是否在每次发送字节后用了低效的延时?是否在循环画点时反复调用设置窗口命令?优化为设置一次窗口,然后连续发送所有像素数据。
  • 检查内存和缓冲区溢出:如果你使用了DMA或大数组,确保数组没有越界。DMA传输的大小计算是否正确(像素数 * 每像素字节数)。

终极调试工具——逻辑分析仪:一个几十块钱的USB逻辑分析仪(配合Sigrok/PulseView软件)是调试SPI/I2C等数字通信的利器。它可以直观地显示SCK、MOSI、DC、CS等信号线上的波形和时序,并能解码出SPI数据。当屏幕不亮时,抓取初始化阶段的波形,看芯片是否收到了正确的命令序列,是定位问题最快的方法。

驱动一块SPI彩屏,从硬件连接到软件调试,是一个典型的嵌入式系统开发流程。它强迫你去阅读数据手册,理解通信协议,配置外设,优化性能。当你最终看到屏幕上显示出你想要的图形和文字时,那种成就感是无与伦比的。这不仅仅是点亮了一块屏幕,更是点亮了你对整个嵌入式系统软硬件协同工作的理解。以此为起点,你可以继续探索更复杂的图形库(如LVGL、emWin),或者将屏幕与传感器结合,做出真正有交互性的作品。