1. 基于STM32的1.47寸IPS彩屏(ST7789V3)驱动移植实战:软件SPI与硬件SPI双方案详解

1. 基于STM32的1.47寸IPS彩屏(ST7789V3)驱动移植实战:软件SPI与硬件SPI双方案详解 基于STM32的1.47寸IPS彩屏(ST7789V3)驱动移植实战软件SPI与硬件SPI双方案详解最近在做一个STM32的小项目需要用到一块小巧的彩色显示屏来显示一些参数和状态。我选了一块1.47寸的IPS屏分辨率是172x320驱动芯片是ST7789V3。东西到手后第一件事就是得把屏幕驱动起来。网上的资料虽然多但要么讲得太笼统要么代码不完整移植起来总得踩几个坑。所以我把自己从拿到屏幕资料到成功点亮再到优化为硬件SPI的整个过程整理出来。这篇文章会手把手带你完成两种驱动方式的移植软件模拟SPI和硬件SPI。无论你是刚接触嵌入式显示还是想优化现有项目的屏幕驱动效率相信都能从中找到清晰的步骤和可用的代码。咱们废话不多说直接开始。1. 准备工作认识屏幕与获取资料1.1 屏幕规格与引脚我用的这块屏幕参数如下你在移植前最好也核对一下参数项规格说明屏幕尺寸1.47英寸分辨率172 (水平) x 320 (垂直) RGB驱动芯片ST7789V3通信接口SPI (串行外设接口)工作电压3.3V工作电流约90mA物理接口8 Pin 排针 (2.54mm间距)这块屏幕通过SPI接口与单片机通信作为SPI从机。它有8个引脚各自有明确的分工引脚名功能说明对应MCU SPI引脚VCC电源正极 (3.3V)接3.3VGND电源地接GNDSCLSPI时钟线SCK (时钟)SDASPI数据线 (主机输出)MOSI (主机输出从机输入)RES复位引脚 (低电平有效)接普通GPIODC数据/命令选择引脚接普通GPIOCS片选引脚 (低电平有效)接普通GPIO (软件控制)BLK背光控制接普通GPIO或直接接3.3V注意RES复位和BLK背光这两个引脚在GPIO紧张时可以简化处理。RES可以接到MCU的复位引脚这样单片机复位时屏幕也跟着复位BLK可以直接接3.3V或悬空代价是失去了通过代码控制背光亮灭的能力。1.2 获取驱动资料驱动移植离不开厂家提供的原始资料里面通常有参考代码和芯片手册。屏幕购买与资料链接我是在淘宝上购买的产品链接和资料都来自同一个地方。资料包我放在了百度网盘。资料下载链接:https://pan.baidu.com/s/15OWpndYzyW8kFPqmfKNfxQ提取码:8888下载后你会找到一个压缩包解压后里面最关键的是一个【LCD】文件夹这里面就是屏幕的驱动源码是我们移植的基础。2. 工程搭建与基础修改拿到资料后第一步是把驱动文件放到我们的STM32工程里并做一些必要的适配。2.1 导入驱动文件在你的STM32工程目录下我用的工程模板是STM32F103C8T6的新建一个文件夹比如就叫LCD。将资料包里【LCD】文件夹中的所有文件主要是lcd.c,lcd.h,lcd_init.c,lcd_init.h复制到你工程新建的LCD文件夹里。打开你的IDE我用的Keil MDK在工程管理器中将lcd.c和lcd_init.c这两个源文件添加到你的工程中。同时记得在IDE的设置里将LCD文件夹的路径添加到头文件包含路径Include Paths中这样编译器才能找到对应的.h文件。2.2 修改头文件引用厂家的例程为了通用性可能会引用一些特定的头文件我们需要根据自己工程的情况来修改。分别打开lcd_init.h和lcd.h文件找到类似#include sys.h的语句将其修改为你工程中实际存在的系统级头文件。根据原始资料这里需要改为board.h。// 在 lcd_init.h 和 lcd.h 中将 #include sys.h // 修改为 #include board.h同样地打开lcd_init.c和lcd.c文件找到#include delay.h也将其修改为#include board.h假设你的延时函数声明在board.h中。2.3 定义数据类型宏为了代码的通用性厂家代码里可能使用了u8,u16,u32这样的简写类型。我们需要在lcd_init.h和lcd.h文件的开头部分通常在#include之后添加这些宏定义确保它们指向STM32标准库或你工程中定义的类型。// 在 lcd_init.h 和 lcd.h 文件中添加 #ifndef u8 #define u8 uint8_t #endif #ifndef u16 #define u16 uint16_t #endif #ifndef u32 #define u32 uint32_t #endif做完以上三步基础的文件准备和适配就完成了。接下来就是核心部分——根据你的硬件连接配置引脚和通信方式。3. 方案一软件模拟SPI驱动软件SPI顾名思义就是用普通的GPIO引脚通过程序代码模拟SPI的时序时钟高低电平变化、数据位输出。它的优点是引脚分配非常灵活任意GPIO都可以用不占用硬件SPI外设。缺点是速度慢并且会占用较多的CPU时间去“模拟”时序。3.1 引脚定义与映射首先你需要决定把屏幕的8个引脚接到STM32的哪几个GPIO上。这里我以全部使用GPIOA为例进行说明你可以根据自己开发板的空闲引脚来调整。打开lcd_init.h文件找到LCD端口定义部分根据你的接线修改宏定义。下面的代码假设了如下连接SCL(时钟) -PA5SDA(数据) -PA7RES(复位) -PA3DC(数据/命令) -PA2CS(片选) -PA4BLK(背光) -PA1//-----------------LCD端口定义---------------- /* SCL PA5 SDA PA7 RES PA3 DC PA2 CS PA4 BLK PA1 */ #define LCD_SCLK_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_5)//SCLSCLK #define LCD_SCLK_Set() GPIO_SetBits(GPIOA,GPIO_Pin_5) #define LCD_MOSI_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_7)//SDAMOSI #define LCD_MOSI_Set() GPIO_SetBits(GPIOA,GPIO_Pin_7) #define LCD_RES_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_3)//RES #define LCD_RES_Set() GPIO_SetBits(GPIOA,GPIO_Pin_3) #define LCD_DC_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_2)//DC #define LCD_DC_Set() GPIO_SetBits(GPIOA,GPIO_Pin_2) #define LCD_CS_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_4)//CS #define LCD_CS_Set() GPIO_SetBits(GPIOA,GPIO_Pin_4) #define LCD_BLK_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_1)//BLK #define LCD_BLK_Set() GPIO_SetBits(GPIOA,GPIO_Pin_1)这些宏定义的作用很简单比如LCD_SCLK_Clr()就是把PA5引脚拉低输出0LCD_SCLK_Set()就是拉高输出1。后续的驱动函数会调用这些宏来产生SPI时序。3.2 GPIO初始化引脚定义好了接下来就要初始化这些GPIO把它们设置为输出模式。修改lcd_init.c文件中的LCD_GPIO_Init(void)函数。void LCD_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; // 使能GPIOA的时钟根据你的MCU型号时钟使能函数可能不同这里是STM32F1的写法 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 配置我们用到的那几个引脚 GPIO_InitStructure.GPIO_Pin GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出模式 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; // 输出速度50MHz GPIO_Init(GPIOA, GPIO_InitStructure);//初始化 // 初始化后将所有引脚设置为高电平根据屏幕逻辑有些引脚高电平是默认状态 GPIO_SetBits(GPIOA, GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7); }提示GPIO_Speed设置为50MHz对于软件模拟SPI来说已经足够这个速度指的是IO口电平翻转的最大速度并不是实际的SPI通信速率。软件SPI的实际速率由你代码中延时决定。至此软件SPI的移植就完成了因为厂家提供的lcd_init.c中的LCD_Writ_Bus等底层函数已经是基于软件模拟SPI的代码我们只需要配置好引脚即可。你可以直接跳到第5节进行验证。4. 方案二硬件SPI驱动如果你对刷屏速度有要求或者希望解放CPU那么硬件SPI是更好的选择。硬件SPI由STM32内部的专用外设生成时钟和时序速度快、稳定性高、不占用CPU。但缺点是引脚固定必须使用MCU指定的具有SPI复用功能的引脚。4.1 硬件SPI引脚选择首先你需要查阅你所使用的STM32型号的数据手册找到其SPI1或其他SPI外设对应的引脚。以常见的STM32F103C8T6的SPI1为例SPI1_SCK(时钟) 可以映射到PA5SPI1_MOSI(主机输出) 可以映射到PA7RES,DC,CS,BLK这四个控制引脚不参与SPI通信时序因此仍然可以使用普通的GPIO我继续沿用PA3,PA2,PA4,PA1。4.2 硬件SPI的GPIO与SPI初始化硬件SPI的初始化分为两部分一是将SCK和MOSI引脚配置为复用推挽输出模式二是配置SPI外设本身的工作模式。修改lcd_init.c中的LCD_GPIO_Init(void)函数为以下内容void LCD_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure_SPI; GPIO_InitTypeDef GPIO_InitStructure_CTRL; SPI_InitTypeDef SPI_InitStructure; // 1. 使能时钟GPIOA和SPI1的时钟都要打开 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); // 2. 配置SPI功能引脚 (PA5: SCK, PA7: MOSI) GPIO_InitStructure_SPI.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure_SPI.GPIO_Mode GPIO_Mode_AF_PP; // 复用推挽输出 GPIO_InitStructure_SPI.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure_SPI); // 3. 配置控制引脚 (RES, DC, CS, BLK) 为普通推挽输出 GPIO_InitStructure_CTRL.GPIO_Pin GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4; GPIO_InitStructure_CTRL.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure_CTRL.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure_CTRL); // 初始化后置高 GPIO_SetBits(GPIOA, GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4); // 4. 配置SPI1外设参数 SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; // 双线全双工虽然只发不收但常用此模式 SPI_InitStructure.SPI_Mode SPI_Mode_Master; // 主机模式 SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; // 数据帧8位 SPI_InitStructure.SPI_CPOL SPI_CPOL_High; // 时钟极性空闲时为高电平 SPI_InitStructure.SPI_CPHA SPI_CPHA_2Edge; // 时钟相位第二个边沿采样 SPI_InitStructure.SPI_NSS SPI_NSS_Soft; // 软件控制NSS片选我们用GPIO控制CS SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_4; // 波特率预分频决定SPI速度 SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; // 高位先发送 SPI_InitStructure.SPI_CRCPolynomial 7; // CRC多项式这里用默认值即可 SPI_Init(SPI1, SPI_InitStructure); // 5. 使能SPI1 SPI_Cmd(SPI1, ENABLE); }关键点解释SPI_CPOL和SPI_CPHA合起来决定了SPI的模式。这里设置为CPOL1, CPHA1即模式3。这个必须和屏幕驱动芯片ST7789的要求一致通常ST7789使用模式3具体请以屏幕资料为准。SPI_BaudRatePrescaler是速度设置。SPI_BaudRatePrescaler_4表示SPI时钟 系统时钟 / 4。如果系统时钟是72MHz那么SPI时钟就是18MHz。你可以根据需求调整这个值但不要超过屏幕芯片支持的最高速率。SPI_NSS_Soft表示我们不用硬件自动管理片选而是用普通的GPIOPA4来控制CS引脚这样更灵活。4.3 修改底层数据发送函数使用硬件SPI后数据发送就不再需要手动模拟时钟了而是通过写SPI的数据寄存器DR来完成。我们需要修改lcd_init.c中的LCD_Writ_Bus(u8 dat)函数它是所有屏幕数据/命令发送的最终出口。/****************************************************************************** 函数说明LCD串行数据写入函数 (硬件SPI版本) 入口数据dat 要写入的串行数据 返回值 无 ******************************************************************************/ void LCD_Writ_Bus(u8 dat) { LCD_CS_Clr(); // 拉低片选开始传输 // 等待发送缓冲区为空确保可以写入新数据 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) RESET); // 将要发送的数据写入SPI数据寄存器硬件会自动发送 SPI_I2S_SendData(SPI1, dat); // 等待接收缓冲区非空对于只发送不接收的情况此步骤是为了确保数据已发送完成 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) RESET); // 读取接收到的数据可能是无效数据以清除标志位 SPI_I2S_ReceiveData(SPI1); LCD_CS_Set(); // 拉高片选结束传输 }这个函数的核心是SPI_I2S_SendData(SPI1, dat)硬件会自动处理时钟生成和数据移位。两个while循环是必要的它们确保了数据被完整地发送出去避免了数据覆盖。4.4 更新引脚定义由于SCK和MOSI现在由硬件SPI控制我们不再需要手动控制它们的电平但lcd_init.h中的宏定义仍然需要保留因为其他函数如LCD_CS_Clr会调用它们。不过LCD_SCLK_Clr/Set和LCD_MOSI_Clr/Set这两个宏在硬件SPI模式下不会被用到可以保留也可以注释掉。为了代码统一我建议先保留。宏定义部分与软件SPI方案完全一致无需修改。至此硬件SPI的移植也完成了5. 移植验证点亮屏幕并显示内容无论你选择哪种方案最后都需要写一个简单的测试程序来验证屏幕是否正常工作。在你的main.c文件中添加如下测试代码#include stm32f10x.h #include board.h // 你的板级支持包头文件 #include lcd.h #include lcd_init.h int main(void) { // 系统初始化包括时钟、延时等 board_init(); // 初始化LCD屏幕 LCD_Init(); // 清屏填充为黑色 LCD_Fill(0, 0, LCD_W, LCD_H, BLACK); float counter 0.0; while(1) { // 在屏幕指定位置显示字符串和数字 LCD_ShowString(0, 32, (u8 *)LCD Width:, WHITE, BLACK, 16, 0); LCD_ShowIntNum(80, 32, LCD_W, 3, WHITE, BLACK, 16); // 显示屏幕宽度 LCD_ShowString(0, 48, (u8 *)LCD Height:, WHITE, BLACK, 16, 0); LCD_ShowIntNum(88, 48, LCD_H, 3, WHITE, BLACK, 16); // 显示屏幕高度 // 显示一个递增的浮点数 LCD_ShowString(0, 64, (u8 *)Count:, WHITE, BLACK, 16, 0); LCD_ShowFloatNum1(48, 64, counter, 2, WHITE, BLACK, 16); // 显示浮点数 counter 0.11; // 延时1秒 delay_ms(1000); } }编译工程下载到开发板上电后你应该能看到屏幕点亮并显示屏幕的宽高信息以及一个不断累加的数字。如果屏幕没有显示或者显示乱码请按以下顺序排查电源和接线确保VCC接3.3VGND接地所有信号线连接牢固。引脚定义仔细核对lcd_init.h中的宏定义是否与你的实际接线完全一致。初始化顺序确保在调用LCD_Init()之前系统的时钟和基本外设已经初始化完毕board_init()。SPI模式如果使用硬件SPI确认CPOL和CPHA的设置与屏幕要求匹配。背光如果屏幕完全不亮检查BLK引脚是否已接高电平3.3V或在代码中将其拉高。成功点亮屏幕只是第一步接下来你可以利用lcd.h中提供的丰富函数如画点、画线、显示图片、汉字等来构建你的图形界面了。希望这篇详细的移植笔记能帮你少走弯路。