0.96寸ST7735驱动IPS彩屏在STM32上的移植与驱动详解
0.96寸ST7735驱动IPS彩屏在STM32上的移植与驱动详解
最近在做一个需要小型显示屏的STM32项目,选来选去,最后用了这块0.96寸的IPS彩屏。它用的是ST7735驱动芯片,分辨率是80x160,尺寸小巧,显示效果还挺细腻。但拿到手后,发现厂家给的例程和自己的工程对不上,移植过程踩了不少坑。今天我就把整个驱动移植的过程,包括软件SPI和硬件SPI两种方式,从头到尾详细捋一遍,希望能帮你省点时间。
这篇教程适合正在给STM32找小屏方案,需要自己动手移植驱动的朋友。我会假设你已经有一个能正常编译的STM32工程(比如用Keil或STM32CubeIDE),并且对GPIO、SPI有基本了解。咱们的目标不是讲原理,而是手把手带你走通移植流程,点亮屏幕。
1. 准备工作:认识屏幕与获取资料
工欲善其事,必先利其器。在动手写代码之前,咱们得先搞清楚这块屏的基本情况,并把必要的资料准备好。
1.1 屏幕规格与引脚
我用的这块屏核心参数如下:
- 屏幕尺寸:0.96英寸
- 分辨率:80 (水平) x 160 (垂直) 像素
- 驱动芯片:ST7735S
- 接口类型:SPI(串行外设接口)
- 工作电压:2.8V ~ 3.3V(直接接3.3V就行)
- 模块尺寸:24mm x 30mm
它通过一个8Pin的排针(2.54mm间距)与主控连接。这8个引脚各自有明确的分工:
| 引脚标号 | 引脚名称 | 方向 | 功能说明 |
|---|---|---|---|
| 1 | GND | - | 电源地 |
| 2 | VCC | 输入 | 电源正极 (2.8V-3.3V) |
| 3 | SCL | 输入 | SPI时钟线 (SCK) |
| 4 | SDA | 输入 | SPI数据线 (MOSI,主机输出从机输入) |
| 5 | RES | 输入 | 复位引脚,低电平有效 |
| 6 | DC | 输入 | 数据/命令选择引脚 (高电平:数据,低电平:命令) |
| 7 | CS | 输入 | 片选引脚,低电平有效 |
| 8 | BLK | 输入 | 背光控制引脚 (高电平点亮,可接PWM调光) |
注意:模块是SPI从机。
SCL对应SPI的SCK,SDA对应SPI的MOSI,CS就是片选NSS。如果MCU的GPIO资源紧张,RES可以接到MCU的复位引脚(这样MCU复位时屏幕也复位),BLK可以直接接3.3V或悬空(代价是无法控制背光亮度)。
1.2 获取驱动资料
驱动移植离不开厂家提供的原始资料。你可以通过以下链接获取:
- 资料下载链接:
https://pan.baidu.com/s/19DxY8JJEzNt4XYF_CwVbDw - 提取码:
8888
下载后,你会找到最重要的几个文件:
- 数据手册:
ZJY096S0800TG01.pdf,里面有详细的电气特性和时序参数。 - 示例代码:一个完整的工程,里面包含了驱动屏幕的核心文件,主要是
lcd.c、lcd.h、lcd_init.c、lcd_init.h。我们的移植工作就是把这些文件“搬”到我们自己的工程里,并做适配。
2. 移植第一步:文件导入与基础适配
拿到厂家例程后,别急着看代码,先把它“放”到我们的工程里。
2.1 导入文件到工程
- 在你的STM32工程目录下(比如
User或Drivers文件夹里),新建一个文件夹,例如叫LCD。 - 将厂家资料里的【LCD】文件夹下的所有源文件(
.c和.h)复制到你刚建的LCD文件夹里。 - 打开你的IDE(如Keil MDK),在项目管理器中,将
lcd.c和lcd_init.c添加到你的工程组(如Application/User)。 - 在IDE的配置中,添加
LCD文件夹的路径到头文件包含路径(Include Paths)里,这样编译器才能找到lcd.h和lcd_init.h。
2.2 修改头文件引用
厂家例程可能使用了它自己定义的系统头文件,我们需要把它改成我们自己工程里用的。通常需要修改两个地方:
- 打开
lcd_init.h文件,找到类似#include “sys.h”的语句,将其改为你自己工程中常用的板级支持包头文件,例如#include “board.h”或#include “main.h”。 - 同样地,打开
lcd.h文件,进行相同的修改。 - 接着,打开
lcd_init.c和lcd.c文件,将里面的#include “delay.h”也替换成#include “board.h”(前提是你的board.h里包含了延时函数声明或直接使用HAL库的HAL_Delay)。
2.3 定义数据类型宏
厂家代码里可能用了u8、u16、u32这样的简写类型。我们需要在lcd_init.h和lcd.h文件的顶部(在所有#include之后,函数声明之前)添加这些宏定义,确保它们指向STM32标准库或HAL库的正确类型。
在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. 核心移植:引脚与通信方式配置
这是移植最关键的一步,决定了屏幕如何与你的STM32芯片“对话”。你可以选择软件模拟SPI或硬件SPI。软件SPI灵活,不挑引脚;硬件SPI速度快,节省CPU。咱们分别讲。
3.1 方案一:使用软件SPI(模拟SPI)
软件SPI就是用普通的GPIO引脚,通过代码控制电平变化来模拟SPI的时序。优点是任意GPIO都能用,缺点是速度慢,且时序靠软件延时,在高主频下可能不稳定。厂家例程默认就是软件SPI。
步骤1:确定连接引脚首先,根据你的开发板原理图和引脚分配,为屏幕的6个控制引脚(RES, DC, CS, BLK, SCL, SDA)分别选定一个STM32的GPIO。记下它们对应的端口和引脚号,例如:
- RES -> PA3
- DC -> PA2
- CS -> PA4
- BLK -> PA1 (也可接3.3V常亮)
- SCL (SCK) -> PA5
- SDA (MOSI) -> PA7
步骤2:修改引脚定义宏打开lcd_init.h文件,找到LCD端口定义部分。你会看到一系列类似#define LCD_RES_Clr() ...的宏。你需要根据你选择的引脚修改它们。
例如,如果你将RES接在PA3,那么修改如下:
//-----------------LCD端口定义---------------- #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) // ... 其他引脚CS、BLK、SCL、SDA依此类推步骤3:编写引脚初始化函数在lcd_init.c中找到LCD_GPIO_Init(void)函数,或者根据lcd_init.h里的声明自己实现一个。这个函数负责初始化你选定的GPIO为输出模式。
下面是一个针对PA1, PA2, PA3, PA4, PA5, PA7的初始化示例(使用标准外设库):
void LCD_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 使能GPIOA时钟 // 配置所有用到的引脚 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); // 初始化后,将CS、RES等引脚置为默认高电平 GPIO_SetBits(GPIOA, GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4); }提示:软件SPI的时序(时钟高低电平、数据建立时间)已经在厂家提供的
LCD_Writ_Bus(u8 dat)等函数中用delay_us之类的函数模拟好了,所以我们只需要配好GPIO就行,不用管SPI外设。
3.2 方案二:使用硬件SPI
硬件SPI利用STM32内部的SPI外设控制器来生成精确的时钟和收发数据,速度快,可靠性高,且不占用CPU时间。但引脚是固定的,必须接到具有SPI复用功能的引脚上。
步骤1:确定硬件SPI引脚以STM32F103C8T6(蓝色小板)为例,SPI1的引脚是:
- PA5 -> SPI1_SCK
- PA6 -> SPI1_MISO (屏幕不需要,可忽略)
- PA7 -> SPI1_MOSI
所以,我们必须将屏幕的SCL接PA5,SDA接PA7。其他控制引脚(RES, DC, CS, BLK)仍然是普通GPIO,可以自由分配(例如继续用PA2, PA3, PA4, PA1)。
步骤2:修改引脚定义宏和软件SPI一样,修改lcd_init.h中的宏定义,但注意SCL和SDA的宏在硬件SPI模式下可能不会被直接调用(因为数据发送改用SPI外设了),但为了代码统一,可以保留或注释掉。重点是确保RES、DC、CS、BLK的宏定义正确。
步骤3:编写硬件SPI初始化函数这是与软件SPI最大的不同。我们需要初始化两部分:SPI外设本身,以及作为普通GPIO的控制引脚。
在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. 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | 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(片选) 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(时钟极性和相位)必须与从设备(ST7735)的要求匹配。通常ST7735的模式是CPOL=1, CPHA=1(即模式3),但具体请以屏幕资料为准。这里例程设置的是High和2Edge,即模式3。
步骤4:修改数据发送函数软件SPI的数据发送是靠模拟时序,硬件SPI则需要调用库函数。找到lcd_init.c文件中的void LCD_Writ_Bus(u8 dat)函数,它是底层字节发送函数,需要重写。
将原来的模拟时序代码替换为使用SPI外设发送的代码:
void LCD_Writ_Bus(u8 dat) { LCD_CS_Clr(); // 拉低片选,开始传输 // 等待发送缓冲区为空(表示可以写入新数据) while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); // 通过SPI1发送一个字节数据 SPI_I2S_SendData(SPI1, dat); // 等待接收缓冲区非空(表示数据已发送完毕,可以读走以清空标志) while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); SPI_I2S_ReceiveData(SPI1); // 读取数据,清空RXNE标志 LCD_CS_Set(); // 拉高片选,结束传输 }这个函数的核心就是调用SPI_I2S_SendData把数据丢给SPI外设,然后等待它发送完成。
4. 上电测试:点亮你的第一块屏
驱动移植好了,最后一步就是写个简单的测试程序,看看屏幕能不能亮起来。
在你的main.c文件中,包含必要的头文件,然后在主循环前初始化屏幕并显示一些内容。
#include "main.h" // 或你的工程主头文件 #include "lcd_init.h" #include "lcd.h" int main(void) { // 系统初始化(时钟、延时等) SystemInit(); delay_init(); // 初始化LCD(里面会调用我们修改好的LCD_GPIO_Init) LCD_Init(); // 清屏为黑色 LCD_Fill(0, 0, LCD_W, LCD_H, BLACK); // 显示一些测试信息 LCD_ShowString(0, 0, (uint8_t *)"Hello STM32!", WHITE, BLACK, 16, 0); LCD_ShowString(0, 20, (uint8_t *)"0.96' IPS Test", BLUE, BLACK, 16, 0); // 画一个矩形框 LCD_DrawRectangle(10, 40, 70, 90, RED); // 填充一个矩形 LCD_Fill(15, 45, 65, 85, GREEN); while (1) { // 可以在这里添加动态刷新内容,比如显示不断变化的数值 // LCD_ShowNum(...); // delay_ms(500); } }编译、下载到开发板。如果一切顺利,你应该能看到屏幕点亮,并显示“Hello STM32!”和彩色的图形。
常见问题排查:
- 白屏或花屏:最常见的是
RES复位时序不对,或者DC引脚在发送命令/数据时电平设置错误。检查LCD_Init()函数里对RES和DC的操作。 - 全黑:检查背光
BLK引脚是否已接高电平(3.3V)。 - 编译错误:回头检查头文件包含路径、数据类型宏定义是否正确。
- 硬件SPI没反应:首先用逻辑分析仪或示波器检查
SCK和MOSI引脚是否有波形。如果没有,检查SPI初始化代码,特别是时钟使能、引脚复用模式、以及SPI_Cmd(ENABLE)是否执行。也可以尝试降低SPI波特率(增大SPI_BaudRatePrescaler的值)。
移植过程虽然繁琐,但一步步理清引脚、通信方式和初始化流程后,其实并不难。希望这篇教程能帮你顺利驱动起这块小巧的IPS屏。在实际项目中,你还可以基于lcd.c中的画点、画线、显示字符函数,封装出更丰富的图形界面。
