梁山派GD32F470驱动0.96寸ST7735彩屏移植实战:从软件SPI到硬件SPI的完整指南

📅 发布时间:2026/7/13 17:14:51 👁️ 浏览次数:
梁山派GD32F470驱动0.96寸ST7735彩屏移植实战:从软件SPI到硬件SPI的完整指南
梁山派GD32F470驱动0.96寸ST7735彩屏移植实战从软件SPI到硬件SPI的完整指南最近在梁山派GD32F470开发板上做项目需要用到一块小巧的彩屏来显示信息于是选了市面上很常见的0.96寸IPS屏驱动芯片是ST7735。这种小屏价格便宜、接口简单非常适合嵌入式设备。但在移植厂家例程到GD32平台时我发现不少朋友会遇到各种编译错误和配置问题特别是软件SPI和硬件SPI的选择与切换。今天我就把自己完整的移植过程分享出来从拿到屏幕资料开始到软件SPI调通再到升级为硬件SPI提升性能手把手带你走一遍。无论你是刚接触嵌入式的新手还是想为GD32找显示屏方案的工程师这篇教程都能帮你快速搞定。1. 准备工作认识你的屏幕在开始写代码之前咱们先搞清楚要驱动的对象是什么。1.1 屏幕基本信息与资料获取我用的这块是0.96寸IPS彩屏驱动芯片是ST7735。它的核心参数如下参数规格工作电压2.8V ~ 3.3V工作电流约30mA屏幕尺寸24mm x 30mm分辨率80 x 160像素 (RGB)通信协议SPI接口引脚8 Pin (2.54mm间距排针)注意屏幕工作电压是3.3V梁山派开发板的IO口也是3.3V电平可以直接连接不需要电平转换。资料从哪里找厂家通常会给一个资料包里面包含数据手册、示例代码等。我用的这块屏资料可以通过以下方式获取资料下载链接:https://pan.baidu.com/s/19DxY8JJEzNt4XYF_CwVbDw提取码:8888拿到资料后你会看到一个压缩包解压后里面通常有LCD文件夹包含lcd.c、lcd.h、lcd_init.c、lcd_init.h等文件这就是我们移植的基础。1.2 屏幕引脚定义这块屏有8个引脚但实际必须接的没那么多。咱们先看看每个引脚是干什么的屏幕引脚功能必须连接吗说明VCC电源(3.3V)是接开发板3.3VGND地是接开发板GNDSCLSPI时钟线是SPI的SCK信号SDASPI数据线是SPI的MOSI信号主机输出RES复位可选可接MCU复位引脚或普通GPIODC数据/命令选择是高电平数据低电平命令CS片选是低电平选中屏幕BLK背光控制可选接3.3V常亮或PWM控制亮度提示如果MCU的GPIO引脚紧张有两个简化方案RES引脚可以直接接到MCU的复位引脚这样MCU复位时屏幕也跟着复位省一个GPIO。BLK引脚可以直接接3.3V让背光常亮代价是无法软件控制亮度。2. 移植第一步工程搭建与基础修改移植的核心思路很简单把厂家提供的代码放到我们的工程里然后根据GD32的库函数和硬件特性进行适配。2.1 导入源码文件首先在你的GD32工程目录下创建一个文件夹比如叫LCD把厂家资料里的lcd.c、lcd.h、lcd_init.c、lcd_init.h这几个文件复制进去。然后在你的IDE比如Keil MDK中将这几个.c文件添加到工程并设置头文件包含路径指向LCD文件夹。2.2 解决编译错误添加完文件后第一次编译肯定会报错。别慌这是正常现象咱们一个个解决。错误1找不到sys.h厂家例程通常基于STM32或其他平台头文件引用不同。打开lcd_init.h和lcd.h找到#include sys.h把它改成GD32的标准头文件// 修改前 #include sys.h // 修改后 #include gd32f4xx.h错误2u8、u16、u32未定义这些是STM32 HAL库常用的类型别名GD32标准库没有。我们需要在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找不到delay.h厂家代码里用delay_ms()函数做延时我们需要替换成自己的延时函数。假设你的工程里有一个systick.h提供了delay_1ms()函数。修改lcd_init.c和lcd.c// 修改前 #include delay.h delay_ms(100); // 修改后 #include systick.h #define delay_ms delay_1ms // 这样代码里所有的delay_ms(100)都会自动替换为delay_1ms(100)解决完这些错误编译应该只剩下引脚相关的错误了接下来就是重头戏——配置引脚。3. 软件SPI移植最直接的驱动方式软件SPI就是用GPIO模拟SPI时序好处是引脚可以任意选不受硬件SPI外设限制适合快速验证。厂家例程默认就是软件SPI咱们先从这个开始。3.1 引脚宏定义为了方便修改我习惯在lcd_init.h里用宏定义所有引脚。下面是我在梁山派上选择的引脚分配// 引脚定义 - 软件SPI版本 #define RCU_LCD_SCL RCU_GPIOB // SCL时钟线 - PB13 #define PORT_LCD_SCL GPIOB #define GPIO_LCD_SCL GPIO_PIN_13 #define RCU_LCD_SDA RCU_GPIOB // SDA数据线 - PB15 #define PORT_LCD_SDA GPIOB #define GPIO_LCD_SDA GPIO_PIN_15 #define RCU_LCD_CS RCU_GPIOB // CS片选 - PB12 #define PORT_LCD_CS GPIOB #define GPIO_LCD_CS GPIO_PIN_12 #define RCU_LCD_DC RCU_GPIOC // DC数据/命令选择 - PC6 #define PORT_LCD_DC GPIOC #define GPIO_LCD_DC GPIO_PIN_6 #define RCU_LCD_RES RCU_GPIOD // RES复位 - PD0 #define PORT_LCD_RES GPIOD #define GPIO_LCD_RES GPIO_PIN_0 #define RCU_LCD_BLK RCU_GPIOC // BLK背光 - PC7 #define PORT_LCD_BLK GPIOC #define GPIO_LCD_BLK GPIO_PIN_7提示这些引脚你可以根据自己板子的实际情况调整只要保证不冲突就行。软件SPI不要求特定引脚。3.2 GPIO初始化函数在lcd_init.c中找到LCD_GPIO_Init()函数修改为GD32的库函数风格void LCD_GPIO_Init(void) { /* 使能所有用到的GPIO时钟 */ rcu_periph_clock_enable(RCU_LCD_SCL); rcu_periph_clock_enable(RCU_LCD_SDA); rcu_periph_clock_enable(RCU_LCD_CS); rcu_periph_clock_enable(RCU_LCD_DC); rcu_periph_clock_enable(RCU_LCD_RES); rcu_periph_clock_enable(RCU_LCD_BLK); /* 配置SCL时钟线为推挽输出 */ gpio_mode_set(PORT_LCD_SCL, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_SCL); gpio_output_options_set(PORT_LCD_SCL, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_SCL); gpio_bit_write(PORT_LCD_SCL, GPIO_LCD_SCL, SET); // 初始化为高电平 /* 配置SDA数据线为推挽输出 */ gpio_mode_set(PORT_LCD_SDA, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_SDA); gpio_output_options_set(PORT_LCD_SDA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_SDA); gpio_bit_write(PORT_LCD_SDA, GPIO_LCD_SDA, SET); /* 配置DC为推挽输出 */ gpio_mode_set(PORT_LCD_DC, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_DC); gpio_output_options_set(PORT_LCD_DC, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_DC); gpio_bit_write(PORT_LCD_DC, GPIO_LCD_DC, SET); /* 配置CS为推挽输出 */ gpio_mode_set(PORT_LCD_CS, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_CS); gpio_output_options_set(PORT_LCD_CS, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_CS); gpio_bit_write(PORT_LCD_CS, GPIO_LCD_CS, SET); /* 配置RES为推挽输出 */ gpio_mode_set(PORT_LCD_RES, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_RES); gpio_output_options_set(PORT_LCD_RES, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_RES); gpio_bit_write(PORT_LCD_RES, GPIO_LCD_RES, SET); /* 配置BLK为推挽输出 */ gpio_mode_set(PORT_LCD_BLK, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_BLK); gpio_output_options_set(PORT_LCD_BLK, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_BLK); gpio_bit_write(PORT_LCD_BLK, GPIO_LCD_BLK, SET); }3.3 修改端口操作宏厂家代码里用宏来控制引脚高低电平我们需要在lcd_init.h中修改这些宏让它们调用GD32的GPIO函数#define LCD_SCLK_Clr() gpio_bit_write(PORT_LCD_SCL, GPIO_LCD_SCL, RESET) // SCL低电平 #define LCD_SCLK_Set() gpio_bit_write(PORT_LCD_SCL, GPIO_LCD_SCL, SET) // SCL高电平 #define LCD_MOSI_Clr() gpio_bit_write(PORT_LCD_SDA, GPIO_LCD_SDA, RESET) // SDA低电平 #define LCD_MOSI_Set() gpio_bit_write(PORT_LCD_SDA, GPIO_LCD_SDA, SET) // SDA高电平 #define LCD_RES_Clr() gpio_bit_write(PORT_LCD_RES, GPIO_LCD_RES, RESET) // RES低电平 #define LCD_RES_Set() gpio_bit_write(PORT_LCD_RES, GPIO_LCD_RES, SET) // RES高电平 #define LCD_DC_Clr() gpio_bit_write(PORT_LCD_DC, GPIO_LCD_DC, RESET) // DC低电平命令 #define LCD_DC_Set() gpio_bit_write(PORT_LCD_DC, GPIO_LCD_DC, SET) // DC高电平数据 #define LCD_CS_Clr() gpio_bit_write(PORT_LCD_CS, GPIO_LCD_CS, RESET) // CS低电平选中 #define LCD_CS_Set() gpio_bit_write(PORT_LCD_CS, GPIO_LCD_CS, SET) // CS高电平取消选中 #define LCD_BLK_Clr() gpio_bit_write(PORT_LCD_BLK, GPIO_LCD_BLK, RESET) // BLK低电平背光关 #define LCD_BLK_Set() gpio_bit_write(PORT_LCD_BLK, GPIO_LCD_BLK, SET) // BLK高电平背光亮到这里软件SPI的移植就完成了编译应该能通过了。你可以先跳到第5节进行验证测试。4. 硬件SPI移植提升性能的关键一步软件SPI虽然简单但有个明显缺点速度慢而且会占用大量CPU时间。对于需要频繁刷新屏幕的应用硬件SPI是更好的选择。4.1 硬件SPI vs 软件SPI有什么区别简单来说软件SPI用代码控制GPIO电平变化来模拟SPI时序。CPU要亲自处理每个时钟边沿效率低。硬件SPIMCU内部有专门的SPI外设硬件你只需要配置好参数、往数据寄存器写数据硬件会自动完成时钟生成、数据移位等所有工作。CPU解放了速度也快得多。梁山派的主控GD32F470ZGT6有6个SPI外设不用就浪费了。4.2 引脚选择与复用功能硬件SPI要求SCL和SDA引脚必须连接到MCU的SPI外设复用引脚上。以SPI1为例查看GD32F470的数据手册可以看到PB13可以复用为SPI1_SCK时钟PB15可以复用为SPI1_MOSI主机输出其他引脚DC、CS、RES、BLK不受限制可以继续用之前的配置。更新lcd_init.h中的引脚定义明确标注复用功能//-----------------LCD端口定义硬件SPI版本---------------- // VCC - 3.3V // SCL - PB13 SPI1_SCK (硬件SPI时钟) // SDA - PB15 SPI1_MOSI (硬件SPI数据输出) // RES - PD0 (可接复位) // DC - PC6 // CS - PB12 (软件控制片选) // BLK - PC7 #define RCU_LCD_SCL RCU_GPIOB // SCK - PB13 #define PORT_LCD_SCL GPIOB #define GPIO_LCD_SCL GPIO_PIN_13 #define RCU_LCD_SDA RCU_GPIOB // MOSI - PB15 #define PORT_LCD_SDA GPIOB #define GPIO_LCD_SDA GPIO_PIN_15 #define RCU_LCD_CS RCU_GPIOB // NSS - PB12 (软件控制) #define PORT_LCD_CS GPIOB #define GPIO_LCD_CS GPIO_PIN_12 #define RCU_LCD_DC RCU_GPIOC // DC - PC6 #define PORT_LCD_DC GPIOC #define GPIO_LCD_DC GPIO_PIN_6 #define RCU_LCD_RES RCU_GPIOD // RES - PD0 #define PORT_LCD_RES GPIOD #define GPIO_LCD_RES GPIO_PIN_0 #define RCU_LCD_BLK RCU_GPIOC // BLK - PC7 #define PORT_LCD_BLK GPIOC #define GPIO_LCD_BLK GPIO_PIN_7 // 硬件SPI相关定义 #define RCU_SPI_HARDWARE RCU_SPI1 // 使用SPI1 #define PORT_SPI SPI1 // SPI外设 #define LINE_AF_SPI GPIO_AF_5 // 复用功能AF54.3 硬件SPI初始化配置硬件SPI的GPIO初始化与软件SPI不同SCL和SDA需要配置为复用功能模式void LCD_GPIO_Init(void) { spi_parameter_struct spi_init_struct; /* 开启所有GPIO时钟 */ rcu_periph_clock_enable(RCU_LCD_SCL); rcu_periph_clock_enable(RCU_LCD_SDA); rcu_periph_clock_enable(RCU_LCD_CS); rcu_periph_clock_enable(RCU_LCD_DC); rcu_periph_clock_enable(RCU_LCD_RES); rcu_periph_clock_enable(RCU_LCD_BLK); /* 使能SPI1时钟 */ rcu_periph_clock_enable(RCU_SPI_HARDWARE); /* 配置SPI的SCK引脚为复用功能 */ gpio_af_set(PORT_LCD_SCL, LINE_AF_SPI, GPIO_LCD_SCL); // 设置复用功能为AF5 gpio_mode_set(PORT_LCD_SCL, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_LCD_SCL); // 复用模式 gpio_output_options_set(PORT_LCD_SCL, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_SCL); gpio_bit_set(PORT_LCD_SCL, GPIO_LCD_SCL); // 初始高电平 /* 配置SPI的MOSI引脚为复用功能 */ gpio_af_set(PORT_LCD_SDA, LINE_AF_SPI, GPIO_LCD_SDA); gpio_mode_set(PORT_LCD_SDA, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_LCD_SDA); gpio_output_options_set(PORT_LCD_SDA, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_SDA); gpio_bit_set(PORT_LCD_SDA, GPIO_LCD_SDA); /* 配置DC、RES、BLK与软件SPI相同 */ gpio_mode_set(PORT_LCD_DC, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_DC); gpio_output_options_set(PORT_LCD_DC, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_DC); gpio_bit_write(PORT_LCD_DC, GPIO_LCD_DC, SET); gpio_mode_set(PORT_LCD_RES, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_RES); gpio_output_options_set(PORT_LCD_RES, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_RES); gpio_bit_write(PORT_LCD_RES, GPIO_LCD_RES, SET); gpio_mode_set(PORT_LCD_BLK, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_LCD_BLK); gpio_output_options_set(PORT_LCD_BLK, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_BLK); gpio_bit_write(PORT_LCD_BLK, GPIO_LCD_BLK, SET); /* 配置CS注意这里还是用GPIO控制不是硬件NSS */ gpio_mode_set(PORT_LCD_CS, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_LCD_CS); gpio_output_options_set(PORT_LCD_CS, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_LCD_CS); gpio_bit_write(PORT_LCD_CS, GPIO_LCD_CS, SET); // 初始不选中 /* 配置SPI参数 */ 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; // 时钟极性相位 spi_init_struct.nss SPI_NSS_SOFT; // 软件控制NSS spi_init_struct.prescale SPI_PSC_2; // 2分频 spi_init_struct.endian SPI_ENDIAN_MSB; // 高位在前 spi_init(PORT_SPI, spi_init_struct); /* 使能SPI */ spi_enable(PORT_SPI); }注意SPI_CK_PL_HIGH_PH_2EDGE这个配置对应时钟空闲为高电平第二个边沿采样。这是ST7735常用的SPI模式如果屏幕不显示可以尝试其他模式如SPI_CK_PL_LOW_PH_1EDGE。4.4 修改数据发送函数这是最关键的一步软件SPI的数据发送是靠GPIO模拟的硬件SPI需要改用SPI外设发送。找到lcd_init.c中的LCD_Writ_Bus()函数这是实际发送字节数据的函数修改如下/****************************************************************************** 函数说明LCD串行数据写入函数硬件SPI版本 入口数据dat 要写入的串行数据 返回值 无 ******************************************************************************/ void LCD_Writ_Bus(u8 dat) { LCD_CS_Clr(); // 片选拉低开始传输 // 等待发送缓冲区空 while(RESET spi_i2s_flag_get(PORT_SPI, SPI_FLAG_TBE)); // 发送数据 spi_i2s_data_transmit(PORT_SPI, dat); // 等待接收完成虽然我们不接收但要等传输完成 while(RESET spi_i2s_flag_get(PORT_SPI, SPI_FLAG_RBNE)); // 读取数据以清除标志实际数据丢弃 spi_i2s_data_receive(PORT_SPI); LCD_CS_Set(); // 片选拉高结束传输 }这个函数做了几件事拉低CS选中屏幕等待SPI发送缓冲区空闲写入要发送的数据等待数据发送完成读取接收寄存器清标志拉高CS取消选中提示这里用的是查询方式简单可靠。如果追求更高效率可以用DMA或中断方式但对于屏幕驱动来说查询方式完全够用。5. 移植验证让屏幕亮起来代码改好了现在写个简单的测试程序验证一下。5.1 主函数测试代码在main.c中写入以下代码#include gd32f4xx.h #include systick.h #include lcd_init.h #include lcd.h int main(void) { float t 0; // 系统初始化 nvic_priority_group_set(NVIC_PRIGROUP_PRE2_SUB2); // 中断优先级分组 systick_config(); // 滴答定时器初始化提供1ms延时 // 屏幕初始化 LCD_Init(); LCD_Fill(0, 0, LCD_W, LCD_H, BLACK); // 清屏为黑色 // 显示固定信息 LCD_ShowString(0, 0, Hello LiangShanPi!, WHITE, BLACK, 16, 0); LCD_ShowString(0, 16, ST7735 0.96 IPS, WHITE, BLACK, 16, 0); while(1) { // 显示屏幕分辨率 LCD_ShowString(0, 16*2, LCD_W:, WHITE, BLACK, 16, 0); LCD_ShowIntNum(48, 16*2, LCD_W, 3, WHITE, BLACK, 16); LCD_ShowString(80, 16*2, LCD_H:, WHITE, BLACK, 16, 0); LCD_ShowIntNum(128, 16*2, LCD_H, 3, WHITE, BLACK, 16); // 显示一个递增的浮点数 LCD_ShowString(0, 16*3, Num:, WHITE, BLACK, 16, 0); LCD_ShowFloatNum1(8*4, 16*3, t, 4, WHITE, BLACK, 16); t 0.11; // 延时1秒 delay_1ms(1000); } }5.2 常见问题排查如果屏幕不亮或者显示异常可以按以下步骤检查电源和接线确认VCC接3.3VGND接地检查所有接线是否牢固特别是SCL、SDA、DC、CS这四根线背光不亮BLK引脚是否接高电平3.3V或者代码里有没有设置LCD_BLK_Set()有些屏幕背光默认是低电平点亮可以试试LCD_BLK_Clr()屏幕全白或全黑检查RES复位时序有些屏幕需要复位脉冲确认SPI模式时钟极性和相位是否正确ST7735常用模式是SPI_CK_PL_HIGH_PH_2EDGE显示错乱检查DC引脚电平发送命令时DC0发送数据时DC1确认字节顺序MSB/LSB是否匹配硬件SPI不工作确认SCL和SDA引脚是否配置为复用功能AF用逻辑分析仪或示波器看SCL是否有时钟信号尝试降低SPI分频比如从SPI_PSC_2改为SPI_PSC_8降低速度5.3 性能对比体验完成硬件SPI移植后你可以明显感受到速度的提升。特别是全屏刷新或者绘制复杂图形时硬件SPI比软件SPI快好几倍而且CPU占用率大大降低。实际项目中如果只是显示静态信息或缓慢更新软件SPI完全够用。但如果需要动画、快速刷新硬件SPI是必须的。我在一个需要实时显示波形图的项目中从软件SPI切换到硬件SPI后刷新率从不到10fps提升到了30fps以上。移植过程中最深的体会是硬件SPI的配置其实并不复杂关键是要理解SPI外设的初始化流程以及如何将原来的GPIO模拟时序替换为硬件发送。一旦调通后面的应用开发就顺畅多了。完整的移植代码可以在立创·梁山派的资料库中找到网盘链接https://pan.baidu.com/s/1pp44yjD1Dhh7U9iZ2a11IA提取码LCKF。里面包含了软件SPI和硬件SPI两个版本的完整工程你可以直接参考使用。