GD32VW553驱动0.96寸IPS彩屏(ST7735)移植与显示实战
GD32VW553驱动0.96寸IPS彩屏(ST7735)移植与显示实战
最近在做一个智能家居的小项目,需要用到一块小巧的彩色显示屏来显示状态信息。我选用了GD32VW553开发板和一块0.96寸的IPS彩屏,屏幕驱动芯片是ST7735。刚开始移植驱动时,确实遇到了一些小麻烦,比如引脚配置、SPI时序、字库显示等问题。今天我就把整个移植过程整理出来,手把手教大家如何让这块屏幕在GD32VW553上跑起来。
这篇文章适合正在使用GD32VW553进行嵌入式开发的工程师或爱好者,特别是那些需要在物联网设备、智能家居面板等应用中添加小型彩色显示屏的朋友。跟着我的步骤走,你就能在自己的项目里轻松显示文字、图形和图片了。
1. 准备工作:了解你的屏幕
在开始写代码之前,咱们先搞清楚要驱动的这块屏幕是什么来头。我用的这块屏参数如下:
- 屏幕尺寸:0.96英寸
- 分辨率:80 x 160 (RGB)
- 驱动芯片:ST7735
- 通信协议:SPI (4线制)
- 工作电压:2.8V ~ 3.3V
- 接口:8 Pin 2.54mm排针
这块屏用的是SPI接口,接线简单,只需要几根线就能驱动。屏幕的显示效果很不错,IPS屏视角广,色彩也鲜艳,很适合做小型设备的UI。
注意:购买屏幕时,商家通常会提供资料包,里面会有详细的规格书和示例代码。我用的这块屏的资料下载链接和提取码在原始资料里,大家可以按需获取。
2. 硬件连接:把屏幕和开发板连起来
驱动屏幕的第一步,就是把线接对。根据原始资料里的引脚定义,我们需要连接8根线。这里我直接给出GD32VW553开发板与屏幕的接线表,大家照着接就行:
| 屏幕引脚 | 开发板引脚 | 功能说明 |
|---|---|---|
| GND | GND | 电源地 |
| VCC | 3V3 | 电源正极 (3.3V) |
| SCL | PA11 | SPI时钟线 (SCK) |
| SDA | PA9 | SPI数据输出线 (MOSI) |
| RES | PA7 | 复位引脚,低电平有效 |
| DC | PA6 | 数据/命令选择引脚 |
| CS | PB11 | SPI片选引脚 |
| BLK | PB12 | 背光控制引脚 |
接线说明:
- SCL (SCK)和SDA (MOSI)是SPI通信必需的时钟线和数据线。
- RES是复位引脚,用来对屏幕进行硬件复位。
- DC这个引脚很关键,它告诉屏幕接下来发送的是命令(低电平)还是数据(高电平)。
- CS是SPI的片选引脚,低电平选中设备。
- BLK控制背光,接高电平背光亮,接低电平背光灭。如果你GPIO引脚紧张,可以直接把它接到3.3V上,这样背光就一直亮着,只是无法调节亮度了。
提示:如果你的项目对GPIO数量要求很苛刻,RES引脚可以接到MCU的复位引脚上,这样MCU复位时屏幕也跟着复位。BLK引脚可以直接接3.3V或悬空(有些模块内部已上拉),代价就是无法用软件控制背光开关了。
3. 软件移植:一步步修改驱动代码
硬件接好后,接下来就是软件部分了。我们的目标是把商家提供的驱动代码移植到GD32VW553的工程中。原始资料里提供了一个完整的例程包,我们需要把它整合到自己的项目里。
3.1 获取并导入源码
首先,从资料包里找到LCD文件夹,里面通常包含lcd_init.c、lcd_init.h、lcd.c、lcd.h、lcdfont.c、lcdfont.h等文件。把这些文件复制到你自己的GD32VW553工程目录下,比如User文件夹里。
然后,在你的IDE(比如Keil或IAR)中,把这些源文件(.c文件)添加到工程,把头文件路径也设置好。
3.2 修改引脚和端口定义
这是移植的核心步骤,我们需要根据之前的硬件连接表,修改驱动代码中的引脚定义。打开lcd_init.h文件,找到端口定义的部分,修改成下面这样:
#ifndef __LCD_INIT_H #define __LCD_INIT_H #include "gd32vw55x.h" #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 //-----------------LCD端口定义---------------- // 使能相关外设时钟 #define LCD_RCU_ENABLE() rcu_periph_clock_enable(RCU_GPIOA); \ rcu_periph_clock_enable(RCU_GPIOB); \ rcu_periph_clock_enable(RCU_SPI); // 根据硬件连接表修改以下定义 #define LCD_SCL_PORT GPIOA #define LCD_SCL_PIN GPIO_PIN_11 #define LCD_SCL_AF GPIO_AF_0 // 复用功能选择,根据数据手册填写 #define LCD_SDA_PORT GPIOA #define LCD_SDA_PIN GPIO_PIN_9 #define LCD_SDA_AF GPIO_AF_0 #define LCD_RES_PORT GPIOA #define LCD_RES_PIN GPIO_PIN_7 #define LCD_DC_PORT GPIOA #define LCD_DC_PIN GPIO_PIN_6 #define LCD_CS_PORT GPIOB #define LCD_CS_PIN GPIO_PIN_11 #define LCD_BLK_PORT GPIOB #define LCD_BLK_PIN GPIO_PIN_12 /* LCD信号控制宏定义 */ #define LCD_RES_Clr() gpio_bit_write(LCD_RES_PORT,LCD_RES_PIN, 0) //RES拉低 #define LCD_RES_Set() gpio_bit_write(LCD_RES_PORT,LCD_RES_PIN, 1) //RES拉高 #define LCD_DC_Clr() gpio_bit_write(LCD_DC_PORT,LCD_DC_PIN, 0) //DC拉低,写命令 #define LCD_DC_Set() gpio_bit_write(LCD_DC_PORT,LCD_DC_PIN, 1) //DC拉高,写数据 #define LCD_CS_Clr() gpio_bit_write(LCD_CS_PORT,LCD_CS_PIN, 0) //CS拉低,选中设备 #define LCD_CS_Set() gpio_bit_write(LCD_CS_PORT,LCD_CS_PIN, 1) //CS拉高,取消选中 #define LCD_BLK_Clr() gpio_bit_write(LCD_BLK_PORT,LCD_BLK_PIN, 0)//背光关闭 #define LCD_BLK_Set() gpio_bit_write(LCD_BLK_PORT,LCD_BLK_PIN, 1)//背光打开 // 函数声明 void LCD_GPIO_Init(void); void LCD_Writ_Bus(u8 dat); void LCD_WR_DATA8(u8 dat); void LCD_WR_DATA(u16 dat); void LCD_WR_REG(u8 dat); void LCD_Address_Set(u16 x1,u16 y1,u16 x2,u16 y2); void LCD_Init(void); #endif关键点解释:
LCD_RCU_ENABLE():这个宏一次性打开了GPIOA、GPIOB和SPI外设的时钟,这是GD32操作外设的第一步。LCD_SCL_AF和LCD_SDA_AF:这是引脚复用功能选择,GPIO_AF_0表示选择AF0功能,具体对应哪个SPI需要查GD32VW553的数据手册。我这里的配置适用于SPI0。- 其他的
Set和Clr宏就是对相应引脚进行高低电平控制,非常直观。
3.3 修改底层通信函数LCD_Writ_Bus
这个函数负责通过SPI发送一个字节的数据,是驱动层最底层的函数。我们需要根据GD32的SPI库函数来重写它。打开lcd_init.c,找到LCD_Writ_Bus函数,修改如下:
void LCD_Writ_Bus(u8 dat) { uint8_t recv_data = 0; LCD_CS_Clr(); // 拉低片选,开始通信 // 等待发送缓冲区为空 while(RESET == spi_flag_get(SPI, SPI_FLAG_TBE)); // 通过SPI发送一个字节数据 spi_data_transmit(SPI, dat); // 等待接收缓冲区非空标志(虽然我们不需要接收的数据,但读一下可以清标志) while(RESET == spi_flag_get(SPI, SPI_FLAG_RBNE)); recv_data = spi_data_receive(SPI); LCD_CS_Set(); // 拉高片选,结束本次通信 return recv_data; }这里用到了GD32标准库的SPI函数。spi_flag_get用来查询状态标志,spi_data_transmit发送数据,spi_data_receive读取数据(主要是为了清除接收标志)。
3.4 修改GPIO和SPI初始化
接下来,我们需要初始化这些引脚和SPI外设。在lcd_init.c中找到LCD_GPIO_Init函数,确保其内容如下:
void LCD_GPIO_Init(void) { // 1. 使能时钟 LCD_RCU_ENABLE(); // 2. 配置SPI引脚(SCL, SDA)为复用功能 gpio_af_set(LCD_SCL_PORT, LCD_SCL_AF, LCD_SCL_PIN); gpio_af_set(LCD_SDA_PORT, LCD_SDA_AF, LCD_SDA_PIN); gpio_mode_set(LCD_SCL_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, LCD_SCL_PIN); gpio_mode_set(LCD_SDA_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, LCD_SDA_PIN); gpio_output_options_set(LCD_SCL_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, LCD_SCL_PIN); gpio_output_options_set(LCD_SDA_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, LCD_SDA_PIN); // 3. 配置控制引脚(RES, DC, CS, BLK)为推挽输出 gpio_mode_set(LCD_RES_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, LCD_RES_PIN); gpio_mode_set(LCD_DC_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, LCD_DC_PIN); gpio_mode_set(LCD_CS_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, LCD_CS_PIN); gpio_mode_set(LCD_BLK_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, LCD_BLK_PIN); gpio_output_options_set(LCD_RES_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, LCD_RES_PIN); gpio_output_options_set(LCD_DC_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, LCD_DC_PIN); gpio_output_options_set(LCD_CS_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, LCD_CS_PIN); gpio_output_options_set(LCD_BLK_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, LCD_BLK_PIN); // 4. 初始化控制引脚状态 LCD_CS_Set(); // 片选默认高(不选中) LCD_DC_Set(); // 默认设置为数据模式 // 5. 配置SPI外设参数 spi_parameter_struct spi_init_struct; spi_struct_para_init(&spi_init_struct); // 初始化结构体 spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX; // 全双工模式 spi_init_struct.device_mode = SPI_MASTER; // 主机模式 spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT; // 8位数据帧 spi_init_struct.clock_polarity_phase = SPI_CK_PL_HIGH_PH_2EDGE; // 时钟极性相位,根据ST7735手册设置 spi_init_struct.nss = SPI_NSS_SOFT; // 软件控制NSS(CS) spi_init_struct.prescale = SPI_PSC_4; // 时钟预分频,决定SPI速度 spi_init_struct.endian = SPI_ENDIAN_MSB; // 高位先发送 // 6. 初始化SPI并使能 spi_init(SPI, &spi_init_struct); spi_enable(SPI); }代码要点:
- 时钟使能:操作任何外设前必须先开时钟。
- 引脚模式:SPI的SCK和MOSI引脚要设置为复用功能(AF),而RES、DC等控制引脚设为推挽输出。
- SPI配置:
SPI_NSS_SOFT表示我们用软件控制CS引脚(即我们之前定义的LCD_CS_Clr/Set)。SPI_PSC_4是时钟分频,需要根据你的系统时钟和屏幕支持的SPI速度来调整。如果屏幕显示异常,可以尝试增大分频值(降低速度)。 - 时钟极性相位:
SPI_CK_PL_HIGH_PH_2EDGE是SPI的模式1 (CPOL=1, CPHA=1),这是ST7735芯片常用的模式,务必确认与你的屏幕规格一致。
3.5 修复中文字库索引问题(关键!)
这是一个很容易踩坑的地方。原始资料里的字库代码可能是为某些编译器准备的,其汉字索引数组是3个字节(为了兼容UTF-8编码),但默认的查找步进是2字节。我们需要修改lcd.c文件中的LCD_ShowChinese函数。
找到这个函数,里面有一行s+=2;,把它改成s+=3;:
void LCD_ShowChinese(u16 x,u16 y,u8 *s,u16 fc,u16 bc,u8 sizey,u8 mode) { while(*s!=0) { if(sizey==12) LCD_ShowChinese12x12(x,y,s,fc,bc,sizey,mode); else if(sizey==16) LCD_ShowChinese16x16(x,y,s,fc,bc,sizey,mode); else if(sizey==24) LCD_ShowChinese24x24(x,y,s,fc,bc,sizey,mode); else if(sizey==32) LCD_ShowChinese32x32(x,y,s,fc,bc,sizey,mode); else return; s+=3; // 将原来的 s+=2; 修改为 s+=3; x+=sizey; } }同时,还需要修改字库文件lcdfont.c。找到字库结构体数组tfont12,tfont16,tfont24,tfont32的定义,将其中的索引数组大小从Index[2]改为Index[3]。
例如,找到typFNT_GB12结构体定义,修改如下:
typedef struct { unsigned char Index[3]; // 将 [2] 改为 [3] unsigned char Msk[24]; } typFNT_GB12;对typFNT_GB16、typFNT_GB24、typFNT_GB32结构体进行同样的修改。
如果不做这个修改,显示中文时会出现乱码或者只能显示第一个字的问题。
4. 编写测试程序:让屏幕亮起来
所有驱动代码修改好后,就可以写个主程序来测试了。在你的main.c文件中,添加头文件并调用初始化函数。
#include "gd32vw55x.h" #include "systick.h" #include "lcd_init.h" #include "lcd.h" // 假设你有一个图片数组,例如 gImage_1 // #include "pic.h" int main(void) { // 系统时钟、延时初始化等 systick_config(); // LCD初始化 LCD_Init(); // 清屏为白色 LCD_Fill(0, 0, LCD_W, LCD_H, WHITE); // 打开背光 LCD_BLK_Set(); while(1) { // 1. 显示中文 LCD_ShowChinese(20, 0, (const u8*)"嵌入式开发", RED, WHITE, 16, 0); // 2. 显示英文字符串 LCD_ShowString(10, 20, (const u8*)"Width:", RED, WHITE, 16, 0); LCD_ShowIntNum(58, 20, LCD_W, 3, RED, WHITE, 16); // 显示屏幕宽度数值 LCD_ShowString(10, 40, (const u8*)"Height:", RED, WHITE, 16, 0); LCD_ShowIntNum(70, 40, LCD_H, 3, RED, WHITE, 16); // 显示屏幕高度数值 // 3. 显示一个递增的浮点数(模拟动态数据) static float counter = 0; LCD_ShowFloatNum1(10, 60, counter, 4, RED, WHITE, 16); // 显示到小数点后两位 counter += 0.11; // 4. 画一些图形 LCD_DrawLine(10, 80, 70, 80, BLUE); // 画线 LCD_DrawRectangle(20, 90, 60, 120, GREEN); // 画矩形框 LCD_Fill(80, 90, 120, 120, YELLOW); // 填充矩形 Draw_Circle(100, 150, 15, RED); // 画圆 // 5. 显示图片 (需要先准备好图片数组并包含头文件) // LCD_ShowPicture(100, 20, 40, 40, gImage_1); delay_1ms(500); // 延时500ms } }编译工程并下载到GD32VW553开发板。如果一切顺利,你应该能看到屏幕上显示出了文字、图形,并且浮点数在不断变化。
5. 调试与常见问题
1. 屏幕白屏或全黑?
- 检查电源和背光:首先用万用表量一下VCC和GND之间是不是3.3V,BLK引脚是否为高电平。
- 检查复位时序:在
LCD_Init()函数里,确保有正确的复位序列(拉低RES一段时间再拉高)。 - 检查SPI速率:尝试将
spi_init_struct.prescale改成一个更大的值(如SPI_PSC_8或SPI_PSC_16),降低SPI速度。
2. 显示花屏、错位?
- 检查DC引脚:确保在发送命令 (
LCD_WR_REG) 前将DC拉低,发送数据 (LCD_WR_DATA) 前将DC拉高。这个时序错误是导致花屏的常见原因。 - 检查屏幕初始化序列:
LCD_Init()函数里有一长串寄存器配置命令,这些是屏幕厂商提供的,一般不要改动。确认这些命令都正确发送了。 - 检查
USE_HORIZONTAL宏:这个宏定义了屏幕的显示方向。如果你希望横屏显示但实际是竖屏,或者坐标不对,可以尝试修改这个值(0/1为竖屏,2/3为横屏)。
3. 中文显示乱码?
- 确保你已经完成了第3.5节的修改,将
s+=2改为s+=3,并修改了字库结构体的索引大小。 - 检查你传入的中文字符串编码。示例代码中的中文是GB2312/GBK编码,如果你的编译器环境默认是UTF-8,可能需要转换或使用其他字库。
4. 显示内容刷新慢?
LCD_Fill或LCD_ShowPicture这种全屏操作会发送大量数据,本身就会慢。可以考虑局部刷新,只更新变化区域。- 优化
LCD_Writ_Bus函数,检查SPI时钟分频是否还能提高(在屏幕允许的范围内)。
移植工作到这里就基本完成了。整个过程的关键在于引脚配置、SPI初始化和底层通信函数的适配。一旦底层打通,上层的图形、文字显示函数都是通用的。你可以基于lcd.c中提供的画点、画线、填充、显示字符等函数,构建更复杂的用户界面。希望这篇教程能帮你顺利点亮手中的小屏幕。
