4.0寸ILI9488彩屏在天空星GD32F407开发板上的16位并口移植实战
最近在做一个项目,需要用到一块4.0寸的彩屏来显示丰富的图形界面。我手头正好有一块ILI9488驱动的4.0寸屏,接口是16位并口,而主控板用的是天空星的GD32F407开发板。网上找了一圈,发现针对这个组合的现成驱动很少,大部分例程都是基于STM32的。没办法,只能自己动手移植了。
这个过程其实挺典型的,很多朋友可能都遇到过类似情况:手上有屏幕,有开发板,但就是缺一个能直接用的驱动。今天我就把这次完整的移植过程分享出来,从资料获取、引脚配置、代码修改到最终验证,一步步带你走通。只要你跟着做,就能让这块4.0寸屏在天空星开发板上亮起来。
1. 准备工作:了解屏幕与获取资料
1.1 屏幕基本信息
我用的这块4.0寸彩屏,核心参数如下:
| 参数项 | 规格说明 |
|---|---|
| 屏幕尺寸 | 4.0英寸 |
| 分辨率 | 320 x 480像素 |
| 驱动芯片 | ILI9488 |
| 通信接口 | 16位并行接口 |
| 工作电压 | 3.3V |
| 接口类型 | 24Pin排针(2.54mm间距) |
注意:原文中屏幕规格部分写的是“驱动芯片:ST7789V”,但根据后续的初始化代码(包含ILI9488特有的寄存器配置)以及标题,实际驱动芯片应为ILI9488。这里以实际移植的芯片为准。
16位并口是什么意思呢?简单说,就是有16根数据线(DB0-DB15)同时传输数据,比常见的SPI(串行)接口快很多,适合刷屏显示。除了数据线,还有几根控制线:CS(片选)、WR(写使能)、RD(读使能)、DC(数据/命令选择)、RES(复位)和BLK(背光控制)。
1.2 获取原始资料
移植的第一步,是找到屏幕厂家提供的原始例程。我是在淘宝上买的屏幕,商品页面提供了百度网盘的资料下载链接。
资料包里最关键的是一个完整的工程,通常是用STM32或其他平台写的。我们的任务就是把这个工程里的屏幕驱动部分“搬”到我们的天空星GD32F407工程里。
拿到资料后,先找到屏幕初始化的核心函数。一般在main.c里会有一个LCD_Init(),这个函数里包含了引脚配置和屏幕的初始化序列,这就是我们要修改的重点。
2. 工程搭建与文件迁移
2.1 复制核心文件
厂家的例程里,驱动文件通常放在一个独立的文件夹里。我们需要把这些文件复制到自己的GD32工程中。
- 在厂家资料里,找到【LCD】这个文件夹,把它整个复制过来。
- 另外,通常还会有一些应用层的文件,比如
GUI.c、GUI.h、test.c和test.h,也一并复制到你的工程目录下。
提示:你的GD32工程需要有一个能用的毫秒级延时函数(比如
delay_ms),因为屏幕初始化需要用到延时。如果还没有,可以先在board.c或类似的文件里实现一个。
2.2 修改头文件与类型定义
把文件添加到工程后,编译肯定会报错,因为原来的代码是针对其他芯片的。别慌,我们一步步改。
首先,修改头文件包含。原工程可能用的是sys.h之类的头文件,我们需要改成GD32的标准头文件。
打开gui.h和lcd.h文件,找到#include "sys.h"这一行,把它改为:
#include "gd32f4xx.h"同样地,在test.h文件里,也添加对GD32头文件的包含:
#include "gd32f4xx.h"接着,处理延时函数。在lcd.c、gui.c和test.c文件中,将#include "delay.h"改为你工程里实际使用的头文件,比如#include "board.h"。
然后,定义数据类型。为了代码兼容,我们需要在lcd.h、gui.h和test.h这三个头文件里,添加以下类型定义宏:
#ifndef u8 #define u8 uint8_t #endif #ifndef u16 #define u16 uint16_t #endif #ifndef u32 #define u32 uint32_t #endif这样,原来代码里的u8、u16、u32这些类型就能在GD32的环境下正常使用了。
做完这些,再编译一下,你会发现错误少了很多,剩下的主要就是引脚相关的错误了。
3. 核心步骤:引脚配置与GPIO初始化
这是移植中最关键的一步,我们要把屏幕的24个引脚,对应到天空星GD32F407开发板的实际GPIO引脚上。
3.1 理解引脚定义
这块屏的24个引脚,可以分为几类:
- 控制线(6根):CS, WR, RD, DC, RES, BLK。这些是控制屏幕读写、复位和背光的。
- 数据线(16根):DB0-DB15。这是传输图像数据的通道,16位并口就是指这16根线。
因为是“软件并口”,意思是我们用普通的GPIO口来模拟这个并口的时序,所以对引脚没有硬件外设的特殊要求(不像硬件FSMC),只要是你开发板上空闲的IO口,基本上都可以用。
3.2 分配GD32F407的引脚
我根据手头开发板的引脚布局,做了如下分配。你可以完全照搬,也可以根据自己板子的情况调整,只要在代码里改一下宏定义就行。
在lcd.h文件中,我添加了一大段宏定义,把每个屏幕引脚都对应到具体的GD32引脚上。这样改起来清晰,以后换引脚也方便。
//-----------------LCD端口移植---------------- // 控制线定义 #define RCU_LCD_RD RCU_GPIOA // RD引脚时钟 #define PORT_LCD_RD GPIOA // RD引脚端口 #define GPIO_LCD_RD GPIO_PIN_3 // RD引脚号 #define RCU_LCD_WR RCU_GPIOA // WR #define PORT_LCD_WR GPIOA #define GPIO_LCD_WR GPIO_PIN_2 #define RCU_LCD_CS RCU_GPIOA // CS #define PORT_LCD_CS GPIOA #define GPIO_LCD_CS GPIO_PIN_0 #define RCU_LCD_DC RCU_GPIOA // DC/RS #define PORT_LCD_DC GPIOA #define GPIO_LCD_DC GPIO_PIN_1 #define RCU_LCD_RES RCU_GPIOA // RES #define PORT_LCD_RES GPIOA #define GPIO_LCD_RES GPIO_PIN_4 #define RCU_LCD_BLK RCU_GPIOD // BLK/背光 #define PORT_LCD_BLK GPIOD #define GPIO_LCD_BLK GPIO_PIN_14 // 数据线定义 DB0-DB15 #define RCU_LCD_DB0 RCU_GPIOA #define PORT_LCD_DB0 GPIOA #define GPIO_LCD_DB0 GPIO_PIN_5 // ... 此处省略DB1-DB15的定义,结构同DB0 // DB3-DB11 我分配在了GPIOE上 // DB12-DB15 我分配在了GPIOB上提示:原文中给出了完整的DB0-DB15定义,这里为了节省篇幅做了省略。你移植时,需要根据自己连接的物理引脚,完整地定义每一个DB引脚对应的RCU、PORT和GPIO_PIN。
3.3 编写GPIO初始化函数
引脚定义好了,接下来就要在lcd.c文件里写一个初始化函数,把这些GPIO都配置成输出模式。
我在lcd.c里新建了一个函数void LCD_GPIO_Init(void)。这个函数很长,但结构是重复的,就是给每个引脚做三件事:
- 使能时钟:告诉单片机这个GPIO模块要开始工作了。
- 设置模式:配置为推挽输出模式,上拉,速度50MHz。推挽输出驱动能力强,适合直接驱动屏幕。
- 设置默认电平:先给一个高电平(SET)。
下面是函数的核心框架,以RD、WR和DB0为例:
void LCD_GPIO_Init(void) { /* 1. 使能所有用到的GPIO时钟 */ rcu_periph_clock_enable(RCU_LCD_RD); rcu_periph_clock_enable(RCU_LCD_WR); // ... 使能其他所有引脚时钟 /* 2. 配置RD引脚 */ gpio_mode_set(PORT_LCD_RD, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_RD); gpio_output_options_set(PORT_LCD_RD, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_RD); gpio_bit_write(PORT_LCD_RD, GPIO_LCD_RD, SET); // 默认拉高 /* 3. 配置WR引脚 */ gpio_mode_set(PORT_LCD_WR, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_WR); gpio_output_options_set(PORT_LCD_WR, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_WR); gpio_bit_write(PORT_LCD_WR, GPIO_LCD_WR, SET); /* 4. 配置数据线DB0 */ gpio_mode_set(PORT_LCD_DB0, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_DB0); gpio_output_options_set(PORT_LCD_DB0, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_DB0); gpio_bit_write(PORT_LCD_DB0, GPIO_LCD_DB0, SET); // ... 按照同样的格式配置DC、CS、RES、BLK以及DB1-DB15引脚 }这个函数虽然代码行数多,但用复制粘贴很快就能完成。确保每一个在宏定义里声明了的引脚,在这里都有对应的配置代码。
4. 修改底层驱动与初始化序列
4.1 修改LCD初始化函数
原来的LCD_Init()函数里包含了针对原开发板的GPIO初始化代码,我们需要把它替换掉。
在lcd.c中找到void LCD_Init(void)函数,将里面关于GPIO初始化的部分(通常是很多行GPIO_Init之类的代码)全部注释掉或者删除,然后换成我们刚刚写好的LCD_GPIO_Init()。
修改后,函数开头看起来是这样的:
void LCD_Init(void) { // 引脚初始化 -- 使用我们为GD32新写的函数 LCD_GPIO_Init(); // 下面原来的一大段STM32的GPIO配置代码可以注释掉了 // GPIO_InitTypeDef GPIO_InitStructure; // RCC_APB2PeriphClockCmd(...); // ... // 屏幕复位序列 LCD_RST_CLR; // 拉低复位引脚 delay_ms(200); LCD_RST_SET; // 拉高复位引脚 delay_ms(50); // 后面的初始化寄存器配置保持不变 Set_Dir(DFT_SCAN_DIR); LCD_WR_REG(0xE0); LCD_WR_DATA(0x00); // ... 后续大量的LCD_WR_REG和LCD_WR_DATA注意:
LCD_RST_CLR、LCD_RST_SET这些宏也需要我们后续定义,它们对应的是控制引脚的高低电平操作。
4.2 重定义控制引脚操作宏
原工程里,控制引脚拉高拉低可能是直接操作寄存器,我们需要把它改成GD32的库函数操作。
在lcd_init.h(或类似的文件)中,找到或添加以下宏定义:
// 控制引脚宏定义 #define LCD_LED(x) gpio_bit_write(PORT_LCD_BLK, GPIO_LCD_BLK, (x)?SET:RESET) // 背光控制 #define LCD_CS_SET gpio_bit_write(PORT_LCD_CS, GPIO_LCD_CS, SET) // CS拉高 #define LCD_RS_SET gpio_bit_write(PORT_LCD_DC, GPIO_LCD_DC, SET) // DC拉高 #define LCD_WR_SET gpio_bit_write(PORT_LCD_WR, GPIO_LCD_WR, SET) // WR拉高 #define LCD_RD_SET gpio_bit_write(PORT_LCD_RD, GPIO_LCD_RD, SET) // RD拉高 #define LCD_RST_SET gpio_bit_write(PORT_LCD_RES, GPIO_LCD_RES, SET) // RES拉高 #define LCD_CS_CLR gpio_bit_write(PORT_LCD_CS, GPIO_LCD_CS, RESET) // CS拉低 #define LCD_RS_CLR gpio_bit_write(PORT_LCD_DC, GPIO_LCD_DC, RESET) // DC拉低 #define LCD_WR_CLR gpio_bit_write(PORT_LCD_WR, GPIO_LCD_WR, RESET) // WR拉低 #define LCD_RD_CLR gpio_bit_write(PORT_LCD_RD, GPIO_LCD_RD, RESET) // RD拉低 #define LCD_RST_CLR gpio_bit_write(PORT_LCD_RES, GPIO_LCD_RES, RESET)// RES拉低这样,原来代码里的LCD_CS=1或LCD_CS=0就会被替换成对应的GD32库函数调用。
4.3 实现16位数据输出函数
这是并口驱动的核心。我们需要一个函数,能把一个16位的数据(比如一个像素的颜色),同时放到16根数据线上。
在lcd_init.h里,我们需要构造一个DATAOUT宏来实现这个功能。思路是:为每一根数据线(DB0-DB15)写一个置位/清零的宏,然后DATAOUT宏依次调用它们。
// 辅助宏:根据x的值设置指定引脚的输出电平 #define PIN_HIGH_OR_LOW(port, gpio, x) {((x) ? (GPIO_OCTL(port)|=(uint32_t)(gpio)) : (GPIO_OCTL(port)&=~(uint32_t)(gpio)));} // 为每一根数据线定义位操作宏 #define BIT_DB0(x) { GPIO_OCTL(PORT_LCD_DB0) &= ~((uint32_t)(GPIO_LCD_DB0)); PIN_HIGH_OR_LOW(PORT_LCD_DB0, GPIO_LCD_DB0, x); } #define BIT_DB1(x) { GPIO_OCTL(PORT_LCD_DB1) &= ~((uint32_t)(GPIO_LCD_DB1)); PIN_HIGH_OR_LOW(PORT_LCD_DB1, GPIO_LCD_DB1, x); } // ... 为DB2到DB15定义类似的宏 // 关键的16位数据输出宏 #define DATAOUT(dat){ \ BIT_DB15( (dat>>15) & 0x01 ); \ BIT_DB14( (dat>>14) & 0x01 ); \ BIT_DB13( (dat>>13) & 0x01 ); \ // ... 处理DB12到DB1 \ BIT_DB0( (dat>>0) & 0x01 ); \ }这个DATAOUT(dat)宏做了什么呢?它接收一个16位的数dat,然后通过右移和与操作,取出每一位的值(0或1),再调用对应的BIT_DBx()宏,将这个值设置到对应的物理引脚上。这样就实现了一次性输出16位数据。
原工程中屏幕的底层写命令、写数据函数(如LCD_WR_REG、LCD_WR_DATA)会调用类似DATAOUT的函数,我们确保它们最终调用的是我们这个新定义的宏即可。
4.4 清理不必要的代码
最后检查一下,原工程里可能有一些我们不需要的功能。
- 如果原工程有触摸屏驱动函数(比如
Touch_Test),而你的屏幕不带触摸或者暂时不用,可以在test.c里把它注释掉。 - 检查是否有针对其他芯片特有的操作(如重映射、特殊时钟配置),如果不是GD32需要的,也可以注释掉。
5. 上电验证与测试
所有代码修改完成后,就可以编译下载了。写一个简单的main函数来测试。
#include "gd32f4xx.h" #include "board.h" #include "LCD.h" #include "test.h" // 包含测试函数 int main(void) { // 设置中断优先级分组(根据你的系统配置) nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 1. 初始化LCD LCD_Init(); // 2. 清屏,填充黄色,看看屏幕是否正常 LCD_Fill(0, 0, 320, 480, YELLOW); // 3. 进入测试循环,显示各种图形和字体 while(1) { main_test(); // 可能是一些主测试流程 Test_Color(); // 颜色测试 Test_FillRec(); // 矩形填充测试 Test_Circle(); // 画圆测试 English_Font_test(); // 英文字体测试 Chinese_Font_test(); // 中文字体测试 Pic_test(); // 图片显示测试 } }上电后,你应该能看到屏幕先被清成黄色,然后开始循环显示各种测试图案、颜色条、几何图形和文字。如果这些都能正常显示,恭喜你,移植成功了!
几个踩过的坑与心得
- 引脚连接一定要仔细:24根线,接错一根可能就白屏或者花屏。最好用不同颜色的排线,并对照引脚定义表反复检查。
- 电源要足:4.0寸屏工作电流有120mA,确保你的开发板3.3V电源能提供足够的电流,否则可能驱动不稳定。
- 初始化时序:
LCD_Init()函数里那一长串寄存器配置值(0xE0, 0x00, 0x07...)是ILI9488芯片的初始化序列,最好不要随意改动,除非你非常了解这个屏的驱动芯片。 - 速度问题:软件模拟并口的速度受限于你CPU操作GPIO的速度。如果发现刷屏很慢,可以检查一下
gpio_output_options_set里设置的速度是否为GPIO_OSPEED_50MHZ,以及优化DATAOUT宏的实现。
移植过程其实就是个“翻译”工作,把屏幕驱动代码从一种单片机“语言”翻译成另一种。理清每个函数、每个引脚的作用,然后对应到GD32的库函数上,耐心一点,都能成功。希望这篇教程能帮你省点时间。