0.96寸彩屏驱动移植实战:基于ST7735的80x160 IPS TFT显示屏在嵌入式平台上的应用
最近在做一个需要小尺寸显示的项目,手头正好有一块0.96寸的IPS彩屏,分辨率是80x160,驱动芯片是ST7735。这种小屏在物联网设备、穿戴设备上用得特别多,但网上的驱动代码质量参差不齐,移植起来经常遇到各种问题。今天我就结合自己实际移植的经验,手把手带你把这套驱动移植到你的嵌入式开发板上,让你少走弯路。
这篇文章适合正在寻找小尺寸彩屏驱动解决方案的嵌入式软件工程师和电子爱好者。我会从硬件引脚连接开始,一步步讲解SPI通信配置、底层驱动函数移植,最后实现显示字符、汉字和图片的功能。跟着做下来,你就能在自己的项目里用上这块性价比很高的显示屏了。
1. 硬件准备与引脚连接
咱们先来看看这块屏的基本情况。我用的这块是0.96寸IPS TFT彩屏,驱动芯片是ST7735,分辨率80x160,通过SPI接口通信。
1.1 屏幕规格参数
| 参数 | 规格 |
|---|---|
| 工作电压 | 2.8~3.3V |
| 工作电流 | 30mA |
| 模块尺寸 | 24(H) x 30(V) mm |
| 像素大小 | 80(H) x 160(V) RGB |
| 驱动芯片 | ST7735 |
| 通信协议 | SPI |
| 管脚数量 | 8 Pin(2.54mm间距排针) |
注意:工作电压一定要在2.8-3.3V之间,超过3.3V可能会损坏屏幕!
1.2 引脚定义与连接
这块屏有8个引脚,每个引脚的作用如下:
- VCC:电源正极(接3.3V)
- GND:电源地
- SCL:SPI时钟线(SCK)
- SDA:SPI数据输出线(MOSI)
- RES:复位引脚(低电平有效)
- DC:数据/命令选择引脚(高电平为数据,低电平为命令)
- CS:片选引脚(低电平有效)
- BLK:背光控制引脚(高电平点亮)
在实际项目中,如果MCU的GPIO引脚不够用,有两个引脚可以简化:
- RES可以接到MCU的复位引脚,这样MCU复位时屏幕也跟着复位
- BLK可以直接接3.3V或悬空,代价是无法控制背光亮度
下面是我在立创开发板上的实际连接方案:
| 屏幕引脚 | 开发板引脚 |
|---|---|
| GND | GND |
| VCC | 3V3 |
| SCL | P302 |
| SDA | P207 |
| RES | P403 |
| DC | P407 |
| CS | P408 |
| BLK | P409 |
2. 驱动代码移植步骤
移植驱动不是简单复制粘贴就行,需要根据你的硬件平台做适配。我总结了一个五步法,跟着做基本不会出错。
2.1 获取原始资料
首先需要下载屏幕的资料和示例代码。原始资料里提供了百度网盘链接,提取码是8888。下载后你会看到这些文件:
- 屏幕规格书(ZJY096S0800TG01.pdf)
- 示例工程代码
- 字库文件
2.2 创建工程并添加文件
在你的开发环境中新建一个工程,然后把下载的驱动文件复制过来。主要需要这几个文件:
lcd_init.h // LCD初始化相关定义 lcd_init.c // LCD初始化实现 lcd.h // LCD应用层函数声明 lcd.c // LCD应用层函数实现 lcdfont.h // 字库数据提示:如果你用的是Keil、IAR或VSCode+PlatformIO,记得在工程设置里添加这些文件的路径。
2.3 修改引脚配置
这是最关键的一步,需要根据你的实际硬件连接修改引脚定义。打开lcd_init.h文件,找到引脚定义部分:
//-----------------LCD端口定义---------------- #define LCD_RES_PIN BSP_IO_PORT_04_PIN_03 // 复位引脚 #define LCD_DC_PIN BSP_IO_PORT_04_PIN_07 // 数据/命令选择 #define LCD_CS_PIN BSP_IO_PORT_04_PIN_08 // 片选 #define LCD_BLK_PIN BSP_IO_PORT_04_PIN_09 // 背光控制这里的BSP_IO_PORT_04_PIN_03对应的是P403引脚。你需要根据自己开发板的引脚命名规则来修改。比如在STM32上,可能会写成GPIO_PIN_3这样的形式。
2.4 配置SPI通信
ST7735通过SPI接口通信,需要配置SPI的时钟、数据位宽等参数。在lcd_init.c的LCD_Init()函数开头,你会看到SPI初始化的代码:
void LCD_Init(void) { fsp_err_t err = R_SPI_Open(&g_spi0_ctrl, g_spi0.p_cfg); if (err != FSP_SUCCESS) { printf("\r\nSPI开启错误\r\n"); } // ... 后续初始化代码 }这里用的是瑞萨RA系列MCU的FSP库。如果你用的是其他平台,比如STM32的HAL库,需要改成对应的SPI初始化函数。
SPI配置要点:
- 时钟极性(CPOL)通常设为0
- 时钟相位(CPHA)通常设为0
- 数据位宽为8位
- 时钟频率建议在10-20MHz之间,太快可能导致通信不稳定
2.5 修改延时函数
驱动里用到了毫秒和微秒延时,原始代码使用的是瑞萨的延时函数:
#ifndef delay_ms #define delay_ms(x) R_BSP_SoftwareDelay(x, BSP_DELAY_UNITS_MILLISECONDS) #endif你需要替换成自己平台的延时函数。比如在STM32 HAL库中,可以改成:
#define delay_ms(x) HAL_Delay(x) #define delay_us(x) delay_us_custom(x) // 需要自己实现微秒延时3. 核心驱动代码解析
移植完成后,咱们来看看这些驱动函数是怎么工作的。理解原理后,调试起来会更容易。
3.1 屏幕初始化序列
LCD_Init()函数里有一长串的寄存器配置,这是ST7735芯片的初始化序列。每个命令都有特定作用:
LCD_WR_REG(0x11); // Sleep out,退出睡眠模式 delay_ms(120); // 需要120ms延时 LCD_WR_REG(0xB1); // 设置正常模式下的帧率 LCD_WR_DATA8(0x05); LCD_WR_DATA8(0x3C); LCD_WR_DATA8(0x3C); LCD_WR_REG(0x36); // 设置扫描方向 if(USE_HORIZONTAL==0)LCD_WR_DATA8(0x08); else if(USE_HORIZONTAL==1)LCD_WR_DATA8(0xC8); else if(USE_HORIZONTAL==2)LCD_WR_DATA8(0x78); else LCD_WR_DATA8(0xA8);注意:初始化序列中的延时很重要!特别是退出睡眠模式后的120ms延时,如果太短屏幕可能无法正常显示。
3.2 显示方向设置
这块屏支持四个方向的显示,通过USE_HORIZONTAL宏定义来控制:
#define USE_HORIZONTAL 2 // 设置横屏或者竖屏显示 0或1为竖屏 2或3为横屏 #if USE_HORIZONTAL==0||USE_HORIZONTAL==1 #define LCD_W 80 #define LCD_H 160 #else #define LCD_W 160 #define LCD_H 80 #endif- 0:竖屏,原点在左上角
- 1:竖屏,原点在右上角
- 2:横屏,原点在左上角
- 3:横屏,原点在左下角
实际使用中我发现,有些屏的起始坐标需要偏移。在LCD_Address_Set()函数里可以看到这些偏移量:
if(USE_HORIZONTAL==0) { LCD_WR_REG(0x2a); // 列地址设置 LCD_WR_DATA(x1+26); // X起始地址+26 LCD_WR_DATA(x2+26); // X结束地址+26 // ... }这个+26的偏移是因为屏幕实际有132列,但只用了中间的80列来显示。
3.3 基本绘图函数
驱动提供了丰富的绘图函数,最基础的是画点函数:
void LCD_DrawPoint(u16 x,u16 y,u16 color) { LCD_Address_Set(x,y,x,y); // 设置光标位置 LCD_WR_DATA(color); // 写入颜色数据 }基于画点函数,实现了画线、画矩形、画圆等高级功能。比如画线函数用的是Bresenham算法,效率很高:
void LCD_DrawLine(u16 x1,u16 y1,u16 x2,u16 y2,u16 color) { u16 t; int xerr=0,yerr=0,delta_x,delta_y,distance; int incx,incy,uRow,uCol; delta_x=x2-x1; // 计算坐标增量 delta_y=y2-y1; uRow=x1; uCol=y1; // ... Bresenham算法实现 }3.4 显示字符和汉字
显示字符需要字库支持,驱动里内置了多种尺寸的ASCII字库和汉字字库:
// 显示一个字符 void LCD_ShowChar(u16 x,u16 y,u8 num,u16 fc,u16 bc,u8 sizey,u8 mode) { u8 temp,sizex,t,m=0; u16 i,TypefaceNum; u16 x0=x; sizex=sizey/2; TypefaceNum=(sizex/8+((sizex%8)?1:0))*sizey; num=num-' '; // 得到偏移后的值 // 根据字号选择不同的字库 if(sizey==12)temp=ascii_1206[num][i]; // 6x12字体 else if(sizey==16)temp=ascii_1608[num][i]; // 8x16字体 else if(sizey==24)temp=ascii_2412[num][i]; // 12x24字体 else if(sizey==32)temp=ascii_3216[num][i]; // 16x32字体 }显示汉字也是类似的原理,只是汉字字库更大。驱动支持12x12、16x16、24x24、32x32四种尺寸的汉字。
4. 实际应用测试
移植完成后,咱们写个简单的测试程序验证一下。在app.c的Run()函数中添加测试代码:
void Run(void) { // 初始化调试串口 UART0_Debug_Init(); printf("\r\n=== 开始LCD测试 ===\r\n"); float t = 0; LCD_Init(); // LCD初始化 LCD_Fill(0, 0, LCD_W, LCD_H, WHITE); // 清屏为白色 while(1) { // 显示汉字 LCD_ShowChinese(40,0,(const char *)"中景园电子",RED,WHITE,16,0); // 显示屏幕尺寸 LCD_ShowString(10,20,(const u8 *)"LCD_W:",RED,WHITE,16,0); LCD_ShowIntNum(58,20,LCD_W,3,RED,WHITE,16); LCD_ShowString(10,40,(const u8 *)"LCD_H:",RED,WHITE,16,0); LCD_ShowIntNum(58,40,LCD_H,3,RED,WHITE,16); // 显示浮点数(递增) LCD_ShowFloatNum1(10,60,t,4,RED,WHITE,16); t+=0.11; // 显示图片(需要提前准备好图片数组) // LCD_ShowPicture(100,20,40,40,gImage_1); delay_ms(100); } }编译下载后,你应该能看到屏幕上显示"中景园电子"和屏幕尺寸信息,还有一个不断增加的浮点数。
5. 常见问题与调试技巧
我在移植过程中遇到过不少坑,这里分享几个常见问题和解决方法:
5.1 屏幕白屏或不显示
可能原因:
- 电源电压不对(必须是2.8-3.3V)
- 复位时序不对
- 初始化序列有误
- SPI通信失败
解决方法:
- 用万用表测量VCC和GND之间的电压
- 检查RESET引脚波形,确保有正确的复位脉冲
- 在
LCD_Init()函数开头加调试信息,确认SPI初始化成功 - 用逻辑分析仪抓取SPI波形,看是否有数据发出
5.2 显示颜色异常
可能原因:
- 颜色格式不对(ST7735是RGB565格式)
- 扫描方向设置错误
- 伽马校正参数有问题
解决方法:
- 确认颜色值是16位的RGB565格式
- 尝试不同的
USE_HORIZONTAL值 - 检查初始化序列中的伽马校正寄存器(0xE0和0xE1)
5.3 显示位置偏移
可能原因:
- 起始坐标偏移量不对
- 显示区域设置错误
解决方法:
- 调整
LCD_Address_Set()函数中的偏移量 - 检查
0x2A和0x2B命令设置的显示区域
5.4 显示速度慢
可能原因:
- SPI时钟频率太低
- 每次画点都设置地址效率低
- 没有使用DMA传输
优化建议:
- 提高SPI时钟频率(但不要超过芯片最大频率)
- 批量传输数据时,只设置一次地址
- 如果MCU支持,使用SPI DMA传输
调试小技巧:在关键函数前后加GPIO翻转,用示波器测量执行时间。比如在
LCD_WR_DATA()函数前后翻转一个IO,就能知道写一个像素需要多长时间。
6. 进阶应用:显示图片和动画
基本的文字显示搞定后,咱们可以玩点更高级的——显示图片和简单动画。
6.1 显示图片
显示图片需要先把图片转换成数组。可以用Img2Lcd这类工具转换:
// 40x40像素的图片数组示例 const unsigned char gImage_1[3200] = { /* 这里是一长串的RGB565数据 */ }; void Show_Image(void) { // 在(100,20)位置显示40x40的图片 LCD_ShowPicture(100, 20, 40, 40, gImage_1); }图片数组的大小计算:宽度 × 高度 × 2字节(RGB565)。40x40的图片就是40×40×2=3200字节。
6.2 简单动画实现
基于画图函数,可以实现简单的动画效果:
void Simple_Animation(void) { u16 x = 0; u16 y = 0; u16 dx = 1; u16 dy = 1; while(1) { // 清除上一帧 LCD_Fill(x, y, x+10, y+10, WHITE); // 更新位置 x += dx; y += dy; // 边界检测 if(x >= LCD_W-10 || x <= 0) dx = -dx; if(y >= LCD_H-10 || y <= 0) dy = -dy; // 绘制新位置 LCD_Fill(x, y, x+10, y+10, RED); delay_ms(50); } }这个例子实现了一个红色方块在屏幕上弹跳的动画。
6.3 优化显示性能
当需要刷新大量内容时,性能优化很重要:
- 局部刷新:只更新变化的部分,而不是整个屏幕
- 双缓冲:在内存中绘制完成后再一次性显示
- 使用硬件加速:如果MCU有LCD控制器,直接使用硬件加速
// 局部刷新示例 void Update_Score(u16 score) { static u16 last_score = 0; // 只更新分数区域 LCD_Fill(50, 0, 100, 16, BLACK); // 清除旧分数 LCD_ShowIntNum(50, 0, score, 3, WHITE, BLACK, 16); last_score = score; }这块0.96寸的ST7735屏虽然小,但功能齐全,在资源受限的嵌入式系统中特别实用。移植过程中最关键的是引脚配置和SPI初始化,这两个搞定了,后面的显示功能就是水到渠成。实际项目中,我一般会先把基本的画点、画线函数调通,然后再逐步添加更复杂的功能。
有个小经验分享:如果屏幕显示异常,先别急着改代码,用逻辑分析仪抓一下SPI波形,很多时候是硬件连接或时序问题。另外,不同批次的屏幕初始化参数可能略有差异,如果遇到问题,可以尝试调整初始化序列中的延时和寄存器值。