
梁山派GD32F470驱动1.47寸ST7789彩屏从软件SPI到硬件SPI的完整移植指南最近在梁山派GD32F470开发板上折腾一块1.47寸的IPS彩屏屏幕驱动芯片是ST7789。网上找的例程大多是软件模拟SPI的虽然能用但刷新速度慢还占CPU。后来我把它改成了硬件SPI驱动效果提升很明显。今天就把这个完整的移植过程从软件SPI到硬件SPI一步步分享给大家特别是给刚接触嵌入式或者GD32的朋友希望能帮你少走点弯路。这块屏幕分辨率是172x320显示效果挺细腻的用SPI协议通信。咱们的目标就是把厂家提供的通用驱动代码完美适配到咱们的梁山派开发板上。1. 准备工作了解屏幕与获取资料1.1 屏幕基本信息我用的这块屏幕参数如下型号1.47寸IPS液晶屏驱动芯片ST7789V3分辨率172(H) × 320(V) RGB工作电压3.3V通信协议SPI接口8 Pin排针2.54mm间距屏幕的引脚虽然标着SCL、SDA但其实它用的是SPI协议SCL对应SPI的时钟线SCKSDA对应SPI的主机输出线MOSI。1.2 资料获取厂家提供了完整的资料包包括原理图、数据手册和示例代码。你可以通过下面的链接获取采购链接淘宝商品链接资料下载https://pan.baidu.com/s/15OWpndYzyW8kFPqmfKNfxQ 提取码8888下载后你会看到一个【LCD】文件夹里面就是驱动源码这是我们移植的基础。2. 移植第一步工程搭建与初步修改2.1 导入工程文件首先在你的GD32工程目录下新建一个文件夹比如叫LCD把厂家资料里的【LCD】文件夹全部复制进去。然后打开你的工程我用的Keil MDK把刚才复制过来的.c文件添加到工程里在工程管理窗口右键点击你的工程分组选择“Add Existing Files to Group...”找到并选中lcd.c和lcd_init.c文件接着添加头文件路径打开工程选项Options for Target进入“C/C”选项卡在“Include Paths”里添加你刚才创建的LCD文件夹路径2.2 解决编译错误第一次编译肯定会报错别慌咱们一个个解决。错误1找不到sys.h厂家代码里引用了sys.h但GD32的标准库用的是gd32f4xx.h。需要修改两个地方打开lcd_init.h和lcd.h文件找到#include sys.h这一行把它改成#include gd32f4xx.h错误2u8、u16、u32未定义GD32的标准库用的是uint8_t、uint16_t这种标准类型定义。咱们在lcd_init.h和lcd.h文件的开头#include语句后面添加类型定义#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.h里的delay_ms()函数做延时。你需要把它换成你自己的毫秒延时函数。在lcd_init.c和lcd.c文件中把#include delay.h改成你自己的延时头文件比如我用的是systick.h把代码里所有的delay_ms()调用改成你的延时函数比如我的是delay_1ms()为了不改动太多地方可以在文件开头加个宏定义#include systick.h #define delay_ms delay_1ms编译到这里应该只剩下引脚相关的错误了接下来咱们就要配置具体的引脚了。3. 引脚配置理解屏幕的8个信号3.1 屏幕引脚功能说明这块屏幕有8个引脚每个都有特定作用引脚名功能说明必须连接吗VCC电源正极3.3V必须GND电源地必须SCLSPI时钟线SCK必须SDASPI数据线MOSI必须RES复位信号建议连接DC数据/命令选择必须CS片选信号必须BLK背光控制可选引脚节省技巧如果你的GPIO引脚紧张有两个引脚可以简化RES引脚可以直接接到MCU的复位引脚上这样MCU复位时屏幕也跟着复位BLK引脚可以接到3.3V或者悬空默认背光全亮代价是不能调节背光亮度3.2 软件SPI引脚配置厂家原来的代码用的是软件模拟SPI就是通过代码控制GPIO电平变化来模拟SPI时序。这种方式简单但速度慢。我选择的引脚分配如下屏幕引脚梁山派引脚引脚功能SCLPB13软件模拟SCKSDAPB15软件模拟MOSIRESPD0复位DCPC6数据/命令选择CSPB12片选BLKPC7背光控制为了代码可读性和便于修改我在lcd_init.h里做了宏定义//-----------------LCD端口移植---------------- //VCC - 3.3V //SCL - PB13 (软件模拟SCK) //SDA - PB15 (软件模拟MOSI) //RES - PD0 //DC - PC6 //CS - PB12 //BLK - PC7 #define RCU_LCD_SCL RCU_GPIOB #define PORT_LCD_SCL GPIOB #define GPIO_LCD_SCL GPIO_PIN_13 #define RCU_LCD_SDA RCU_GPIOB #define PORT_LCD_SDA GPIOB #define GPIO_LCD_SDA GPIO_PIN_15 #define RCU_LCD_CS RCU_GPIOB #define PORT_LCD_CS GPIOB #define GPIO_LCD_CS GPIO_PIN_12 #define RCU_LCD_DC RCU_GPIOC #define PORT_LCD_DC GPIOC #define GPIO_LCD_DC GPIO_PIN_6 #define RCU_LCD_RES RCU_GPIOD #define PORT_LCD_RES GPIOD #define GPIO_LCD_RES GPIO_PIN_0 #define RCU_LCD_BLK RCU_GPIOC #define PORT_LCD_BLK GPIOC #define GPIO_LCD_BLK GPIO_PIN_7然后在lcd_init.c中修改LCD_GPIO_Init()函数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); }最后在lcd_init.h中修改引脚操作宏定义#define LCD_SCLK_Clr() gpio_bit_write(PORT_LCD_SCL, GPIO_LCD_SCL, RESET) // SCL0 #define LCD_SCLK_Set() gpio_bit_write(PORT_LCD_SCL, GPIO_LCD_SCL, SET) // SCL1 #define LCD_MOSI_Clr() gpio_bit_write(PORT_LCD_SDA, GPIO_LCD_SDA, RESET) // SDA0 #define LCD_MOSI_Set() gpio_bit_write(PORT_LCD_SDA, GPIO_LCD_SDA, SET) // SDA1 #define LCD_RES_Clr() gpio_bit_write(PORT_LCD_RES, GPIO_LCD_RES, RESET) // RES0 #define LCD_RES_Set() gpio_bit_write(PORT_LCD_RES, GPIO_LCD_RES, SET) // RES1 #define LCD_DC_Clr() gpio_bit_write(PORT_LCD_DC, GPIO_LCD_DC, RESET) // DC0 #define LCD_DC_Set() gpio_bit_write(PORT_LCD_DC, GPIO_LCD_DC, SET) // DC1 #define LCD_CS_Clr() gpio_bit_write(PORT_LCD_CS, GPIO_LCD_CS, RESET) // CS0 #define LCD_CS_Set() gpio_bit_write(PORT_LCD_CS, GPIO_LCD_CS, SET) // CS1 #define LCD_BLK_Clr() gpio_bit_write(PORT_LCD_BLK, GPIO_LCD_BLK, RESET) // BLK0 #define LCD_BLK_Set() gpio_bit_write(PORT_LCD_BLK, GPIO_LCD_BLK, SET) // BLK1到这里软件SPI的移植就完成了。编译一下应该能通过了。4. 升级到硬件SPI提升性能的关键4.1 为什么用硬件SPI软件SPI是用GPIO模拟时序每次发送一个bit都要CPU参与效率低。硬件SPI是MCU内置的专用外设发送数据时CPU只需要把数据扔给SPI控制器剩下的时序、时钟生成都由硬件完成。梁山派用的GD32F470有6个SPI外设不用就浪费了。硬件SPI的优势速度快最高能到几十MHz占用CPU少发送数据时CPU可以干别的事稳定性好时序由硬件保证更可靠4.2 硬件SPI引脚选择硬件SPI需要把SCL和SDA接到MCU的SPI功能引脚上。查一下GD32F470的数据手册第28页PB13和PB15正好是SPI1的SCK和MOSI引脚。我的引脚分配屏幕引脚梁山派引脚引脚功能SCLPB13SPI1_SCKSDAPB15SPI1_MOSIRESPD0复位DCPC6数据/命令选择CSPB12片选软件控制BLKPC7背光控制注意CS片选引脚我仍然用GPIO软件控制而不是SPI硬件NSS。这样更灵活可以控制多个SPI设备。4.3 硬件SPI初始化配置首先在lcd_init.h中添加SPI相关的宏定义// 在前面GPIO定义的基础上添加SPI相关定义 #define RCU_SPI_HARDWARE RCU_SPI1 // SPI1时钟 #define PORT_SPI SPI1 // SPI1外设 #define LINE_AF_SPI GPIO_AF_5 // 复用功能AF5然后重写LCD_GPIO_Init()函数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引脚PB13为复用功能 */ gpio_af_set(PORT_LCD_SCL, LINE_AF_SPI, GPIO_LCD_SCL); 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引脚PB15为复用功能 */ 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引脚普通GPIO输出 */ 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); /* 配置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); /* 配置CS引脚注意这里不用上拉用NONE */ 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); /* 配置SPI1参数 */ 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; // 时钟极性相位 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); /* 使能SPI1 */ spi_enable(PORT_SPI); }这里有几个关键点需要注意GPIO复用功能SCK和MOSI要设置为GPIO_MODE_AF复用模式并用gpio_af_set()指定复用功能号SPI配置SPI_NSS_SOFT表示用软件控制片选这样我们可以手动控制CS引脚时钟分频SPI_PSC_2是2分频系统时钟是200MHz的话SPI时钟就是100MHz。如果屏幕不支持这么高可以加大分频值4.4 修改数据发送函数厂家原来的LCD_Writ_Bus()函数是用软件模拟SPI时序的现在要改成硬件SPI发送。在lcd_init.c中找到LCD_Writ_Bus()函数修改为/****************************************************************************** 函数说明LCD串行数据写入函数硬件SPI版本 入口数据dat 要写入的串行数据 返回值 无 ******************************************************************************/ void LCD_Writ_Bus(u8 dat) { LCD_CS_Clr(); // 拉低CS选中屏幕 // 等待发送缓冲区空 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取消选中 }这个函数的工作流程拉低CS片选信号告诉屏幕我要发数据了等待SPI发送缓冲区为空确保可以发送新数据把数据写入SPI数据寄存器硬件会自动发送等待发送完成通过检查接收缓冲区非空标志读取一下数据寄存器清除标志位拉高CS结束本次传输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(); // 初始化LCD屏幕 LCD_Init(); // 清屏为黑色 LCD_Fill(0, 0, LCD_W, LCD_H, BLACK); 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 可能遇到的问题及解决屏幕不亮检查BLK背光引脚是否接好可以尝试直接接到3.3V花屏或乱码检查SPI时钟极性相位配置ST7789通常用SPI_CK_PL_HIGH_PH_2EDGE通信失败用逻辑分析仪或示波器看SCK和MOSI波形确认时序正确刷新慢如果是硬件SPI还慢尝试减小SPI分频值提高时钟频率5.3 性能对比移植完成后你可以明显感受到软件SPI刷新一屏数据需要几百毫秒CPU占用率高硬件SPI刷新速度提升10倍以上CPU几乎不参与数据传输如果需要更快的刷新速度还可以考虑使用DMA配合SPI那样CPU就完全解放了。移植成功的完整工程代码可以通过下面的链接下载参考 链接: https://pan.baidu.com/s/1pp44yjD1Dhh7U9iZ2a11IA 提取码: LCKF实际项目中硬件SPI的稳定性确实比软件模拟好很多特别是长时间运行或者需要高速刷新的场合。如果你在移植过程中遇到其他问题可以重点检查引脚配置和SPI参数这两个地方最容易出问题。