GD32VW553驱动0.96寸SPI单色OLED屏(SSD1306)移植与实战
最近在玩GD32VW553这块RISC-V的开发板,想给它接个小屏幕显示点信息,0.96寸的OLED屏是个不错的选择,小巧省电。网上找了一圈,发现很多STM32的驱动代码,但直接用在GD32VW553上不行,得自己动手移植。今天我就把整个移植过程,从硬件连接到代码修改,一步步分享给大家,保证你跟着做就能点亮屏幕。
1. 硬件准备与连接
1.1 认识我们的OLED屏
咱们用的这块屏是市面上最常见的0.96寸OLED,驱动芯片是SSD1306,通信接口是SPI。先看看它的基本参数:
| 参数 | 规格 |
|---|---|
| 工作电压 | 3.3V |
| 工作电流 | 约15mA |
| 模块尺寸 | 27.3 x 27.8 mm |
| 分辨率 | 128 x 64 像素 |
| 驱动芯片 | SSD1306 |
| 通信协议 | SPI |
| 管脚数量 | 7 Pin (2.54mm间距排针) |
注意:这块屏是3.3V供电的,千万别接5V,会烧掉!
1.2 引脚定义与连接
OLED模块有7个引脚,每个引脚的作用如下:
| OLED引脚 | 功能说明 | 连接GD32VW553引脚 |
|---|---|---|
| GND | 电源地 | GND |
| VCC | 电源正(3.3V) | 3V3 |
| SCL | SPI时钟线(SCK) | PA11 |
| SDA | SPI数据线(MOSI) | PA9 |
| RES | 复位信号 | PA7 |
| DC | 数据/命令选择 | PA6 |
| CS | 片选信号 | PB11 |
这里我选择的引脚都是GD32VW553上比较常用的GPIO,你也可以根据自己需求调整,只要保证SPI引脚对应正确就行。
连接的时候要注意:
- 先断电再接线,防止短路
- 杜邦线要插牢,接触不良最头疼
- 电源和地线一定要接对
2. 工程搭建与文件准备
2.1 获取驱动源码
首先需要找到OLED的驱动代码。我用的这份代码是从一个通用的SSD1306驱动修改过来的,包含了基本的显示功能。
提示:如果你手头没有现成的驱动代码,可以到文章开头提到的资料链接下载,提取码是8888。
2.2 创建工程模板
如果你还没有GD32VW553的工程,建议先创建一个基础工程模板。GD32官方提供了完整的开发环境配置指南,这里我就不赘述了。
关键步骤:
- 使用GD32VW553的官方固件库
- 配置好系统时钟和延时函数
- 确保SPI外设能正常使用
2.3 添加OLED驱动文件
把下载的OLED驱动文件复制到你的工程目录里,主要需要两个文件:
oled.c- 驱动实现文件oled.h- 头文件,包含函数声明和宏定义
在工程中添加这两个文件,然后在main.c中包含oled.h头文件。
3. 驱动代码移植与修改
3.1 修改头文件配置
首先打开oled.h文件,我们需要修改引脚定义和数据类型定义。
#ifndef __OLED_H #define __OLED_H #include "gd32vw55x.h" #include "systick.h" // 定义常用的数据类型 #ifndef u8 #define u8 uint8_t #endif #ifndef u16 #define u16 uint16_t #endif #ifndef u32 #define u32 uint32_t #endif // 延时函数宏定义 #ifndef delay_ms #define delay_ms(x) delay_1ms(x) #endif #ifndef delay_us #define delay_us(x) delay_1us(x) #endif //-----------------OLED端口定义---------------- // 使能相关外设时钟 #define OLED_RCU_ENABLE() rcu_periph_clock_enable(RCU_GPIOA); \ rcu_periph_clock_enable(RCU_GPIOB); \ rcu_periph_clock_enable(RCU_SPI) // SPI引脚定义 #define OLED_SCL_PORT GPIOA #define OLED_SCL_PIN GPIO_PIN_11 #define OLED_SCL_AF GPIO_AF_0 #define OLED_SDA_PORT GPIOA #define OLED_SDA_PIN GPIO_PIN_9 #define OLED_SDA_AF GPIO_AF_0 // 控制引脚定义 #define OLED_RES_PORT GPIOA #define OLED_RES_PIN GPIO_PIN_7 #define OLED_DC_PORT GPIOA #define OLED_DC_PIN GPIO_PIN_6 #define OLED_CS_PORT GPIOB #define OLED_CS_PIN GPIO_PIN_11 // 控制引脚操作宏 #define OLED_RES_Clr() gpio_bit_write(OLED_RES_PORT, OLED_RES_PIN, 0) // RES低电平 #define OLED_RES_Set() gpio_bit_write(OLED_RES_PORT, OLED_RES_PIN, 1) // RES高电平 #define OLED_DC_Clr() gpio_bit_write(OLED_DC_PORT, OLED_DC_PIN, 0) // DC低电平 #define OLED_DC_Set() gpio_bit_write(OLED_DC_PORT, OLED_DC_PIN, 1) // DC高电平 #define OLED_CS_Clr() gpio_bit_write(OLED_CS_PORT, OLED_CS_PIN, 0) // CS低电平 #define OLED_CS_Set() gpio_bit_write(OLED_CS_PORT, OLED_CS_PIN, 1) // CS高电平 // 命令/数据模式定义 #define OLED_CMD 0 // 写命令 #define OLED_DATA 1 // 写数据 // 函数声明 void OLED_Init(void); void OLED_Clear(void); void OLED_Refresh(void); void OLED_ShowChar(u8 x, u8 y, u8 chr, u8 size1, u8 mode); void OLED_ShowString(u8 x, u8 y, u8 *chr, u8 size1, u8 mode); void OLED_ShowChinese(u8 x, u8 y, u8 num, u8 size1, u8 mode); void OLED_ShowNum(u8 x, u8 y, u32 num, u8 len, u8 size1, u8 mode); void OLED_ShowPicture(u8 x, u8 y, u8 sizex, u8 sizey, u8 BMP[], u8 mode); // ... 其他函数声明 #endif这里有几个关键点需要注意:
- 把原来的
#include "sys.h"改成了#include "systick.h",因为GD32的延时函数在systick.h里 - 引脚定义要和你实际连接的引脚一致
- GPIO复用功能
GPIO_AF_0表示SPI功能,这是GD32的配置方式
3.2 修改初始化函数
接下来修改oled.c中的OLED_Init()函数,这是驱动的核心初始化部分。
void OLED_Init(void) { // 1. 使能相关外设时钟 OLED_RCU_ENABLE(); // 2. 配置SPI引脚为复用功能 gpio_af_set(OLED_SCL_PORT, OLED_SCL_AF, OLED_SCL_PIN); gpio_af_set(OLED_SDA_PORT, OLED_SDA_AF, OLED_SDA_PIN); // 3. 设置引脚模式 gpio_mode_set(OLED_SCL_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, OLED_SCL_PIN); gpio_mode_set(OLED_SDA_PORT, GPIO_MODE_AF, GPIO_PUPD_NONE, OLED_SDA_PIN); gpio_mode_set(OLED_RES_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, OLED_RES_PIN); gpio_mode_set(OLED_DC_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, OLED_DC_PIN); gpio_mode_set(OLED_CS_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, OLED_CS_PIN); // 4. 设置输出选项 gpio_output_options_set(OLED_SCL_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, OLED_SCL_PIN); gpio_output_options_set(OLED_SDA_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, OLED_SDA_PIN); gpio_output_options_set(OLED_RES_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, OLED_RES_PIN); gpio_output_options_set(OLED_DC_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, OLED_DC_PIN); gpio_output_options_set(OLED_CS_PORT, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, OLED_CS_PIN); // 5. 初始化控制引脚状态 OLED_CS_Set(); // 片选拉高,初始不选中 OLED_DC_Set(); // 数据/命令线拉高 // 6. 配置SPI参数 spi_parameter_struct spi_init_struct; /* 配置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; // 软件控制片选 spi_init_struct.prescale = SPI_PSC_32; // 32分频 spi_init_struct.endian = SPI_ENDIAN_MSB; // 高位在前 // 7. 初始化SPI spi_init(&spi_init_struct); /* 使能SPI */ spi_enable(); // 8. OLED硬件复位 OLED_RES_Clr(); // 复位引脚拉低 delay_ms(200); // 保持低电平200ms OLED_RES_Set(); // 复位引脚拉高 // 9. 发送初始化命令序列 OLED_WR_Byte(0xAE, OLED_CMD); // 关闭显示 OLED_WR_Byte(0x00, OLED_CMD); // 设置低列地址 OLED_WR_Byte(0x10, OLED_CMD); // 设置高列地址 OLED_WR_Byte(0x40, OLED_CMD); // 设置起始行地址 OLED_WR_Byte(0x81, OLED_CMD); // 设置对比度控制 OLED_WR_Byte(0xCF, OLED_CMD); // 设置对比度值 OLED_WR_Byte(0xA1, OLED_CMD); // 设置段重映射,正常显示 OLED_WR_Byte(0xC8, OLED_CMD); // 设置COM扫描方向,正常显示 OLED_WR_Byte(0xA6, OLED_CMD); // 设置正常显示 OLED_WR_Byte(0xA8, OLED_CMD); // 设置多路复用比率 OLED_WR_Byte(0x3f, OLED_CMD); // 1/64占空比 OLED_WR_Byte(0xD3, OLED_CMD); // 设置显示偏移 OLED_WR_Byte(0x00, OLED_CMD); // 无偏移 OLED_WR_Byte(0xd5, OLED_CMD); // 设置显示时钟分频比 OLED_WR_Byte(0x80, OLED_CMD); // 设置时钟为100帧/秒 OLED_WR_Byte(0xD9, OLED_CMD); // 设置预充电周期 OLED_WR_Byte(0xF1, OLED_CMD); // 预充电15个时钟,放电1个时钟 OLED_WR_Byte(0xDA, OLED_CMD); // 设置COM引脚硬件配置 OLED_WR_Byte(0x12, OLED_CMD); // 备用配置 OLED_WR_Byte(0xDB, OLED_CMD); // 设置VCOMH OLED_WR_Byte(0x40, OLED_CMD); // 设置VCOM取消选择级别 OLED_WR_Byte(0x20, OLED_CMD); // 设置页寻址模式 OLED_WR_Byte(0x02, OLED_CMD); // 页寻址模式 OLED_WR_Byte(0x8D, OLED_CMD); // 电荷泵设置 OLED_WR_Byte(0x14, OLED_CMD); // 使能电荷泵 OLED_WR_Byte(0xA4, OLED_CMD); // 禁用整个显示开启 OLED_WR_Byte(0xA6, OLED_CMD); // 禁用反色显示 // 10. 清屏并开启显示 OLED_Clear(); OLED_WR_Byte(0xAF, OLED_CMD); // 开启显示 }这个初始化函数做了很多事情,我简单解释一下关键步骤:
- 步骤1-4:配置GPIO,SPI引脚要设置为复用功能,控制引脚设置为推挽输出
- 步骤5:初始化控制引脚状态,防止误操作
- 步骤6-7:配置SPI参数,这里用的是软件控制片选,时钟32分频
- 步骤8:硬件复位OLED,这是必须的,确保OLED从确定状态开始工作
- 步骤9:发送一系列初始化命令,这些命令是SSD1306芯片规定的
- 步骤10:清屏并开启显示
注意:SPI的时钟极性相位配置
SPI_CK_PL_HIGH_PH_2EDGE要和OLED屏的时序要求一致,不同厂家的屏可能不同。
3.3 修改字节写入函数
SPI通信的核心是OLED_WR_Byte()函数,它负责通过SPI发送一个字节的数据或命令。
void OLED_WR_Byte(u8 dat, u8 cmd) { uint8_t recv_data = 0; // 1. 设置DC引脚:命令模式还是数据模式 if(cmd) OLED_DC_Set(); // 数据模式 else OLED_DC_Clr(); // 命令模式 // 2. 拉低片选,选中OLED OLED_CS_Clr(); // 3. 等待发送缓冲区为空 while(RESET == spi_flag_get(SPI_FLAG_TBE)); // 4. 通过SPI发送数据 spi_data_transmit(dat); // 5. 等待接收缓冲区不为空(全双工模式需要读取) while(RESET == spi_flag_get(SPI_FLAG_RBNE)); recv_data = spi_data_receive(); // 6. 拉高片选,取消选中 OLED_CS_Set(); OLED_DC_Set(); // 恢复DC为高电平 }这个函数的关键点:
- DC引脚控制:OLED通过DC引脚区分发送的是命令还是数据,0是命令,1是数据
- 片选控制:每次通信前要拉低CS,通信完成后拉高
- SPI发送:使用GD32的SPI库函数发送数据
- 接收数据:全双工模式下发送的同时也会接收,虽然用不上但要读取以清空缓冲区
4. 编写测试程序
4.1 主函数测试代码
现在驱动已经移植好了,我们来写个测试程序验证一下。在main.c中添加以下代码:
#include "gd32vw55x.h" #include "systick.h" #include <stdio.h> #include "main.h" #include "gd32vw553h_eval.h" #include "oled.h" #include "bmp.h" // 如果有图片数据的话 int main(void) { // 1. 系统初始化 systick_config(); // 配置系统滴答定时器 eclic_priority_group_set(ECLIC_PRIGROUP_LEVEL3_PRIO1); // 2. 初始化外设(可选) gd_eval_led_init(LED1); gd_eval_com_init(EVAL_COM0); // 3. 初始化OLED OLED_Init(); OLED_ColorTurn(0); // 0正常显示,1反色显示 OLED_DisplayTurn(0); // 0正常方向,1旋转180度 uint8_t t = ' '; // 用于显示变化的ASCII字符 while(1) { // 测试1:显示图片(如果有图片数据) // OLED_ShowPicture(0, 0, 128, 64, BMP1, 1); // OLED_Refresh(); // delay_1ms(1000); // OLED_Clear(); // 测试2:显示汉字 OLED_ShowChinese(0, 0, 0, 16, 1); // 显示第0个汉字,16x16大小 OLED_ShowChinese(18, 0, 1, 16, 1); // 显示第1个汉字 OLED_ShowChinese(36, 0, 2, 16, 1); // 显示第2个汉字 OLED_ShowChinese(54, 0, 3, 16, 1); // 显示第3个汉字 OLED_ShowChinese(72, 0, 4, 16, 1); // 显示第4个汉字 // 测试3:显示英文字符串 OLED_ShowString(8, 16, "GD32VW553", 16, 1); OLED_ShowString(20, 32, "OLED Test", 16, 1); // 测试4:显示ASCII字符 OLED_ShowString(0, 48, "ASCII:", 16, 1); OLED_ShowChar(48, 48, t, 16, 1); // 显示ASCII字符 t++; if(t > '~') t = ' '; // 循环显示 // 测试5:显示数字 OLED_ShowNum(90, 48, 1234, 4, 16, 1); OLED_Refresh(); // 更新显示 delay_1ms(500); // 延时500ms OLED_Clear(); // 清屏 // 测试6:不同大小的字符 OLED_ShowString(0, 0, "ABC", 8, 1); // 6x8字体 OLED_ShowString(0, 8, "ABC", 12, 1); // 6x12字体 OLED_ShowString(0, 20, "ABC", 16, 1); // 8x16字体 OLED_ShowString(0, 36, "ABC", 24, 1); // 12x24字体 OLED_Refresh(); delay_1ms(500); OLED_Clear(); } }4.2 添加字库文件
要显示汉字和字符,还需要字库文件。通常是一个oledfont.h文件,里面包含了各种字体的点阵数据。
如果你没有现成的字库,可以自己生成或者从网上下载。字库文件通常包含:
- ASCII字符的点阵数据(6x8, 8x16等)
- 常用汉字的点阵数据(16x16, 24x24等)
在oled.c中需要包含这个字库文件:
#include "oledfont.h"5. 常见问题与调试技巧
5.1 屏幕不亮或显示异常
如果屏幕完全不亮,按以下步骤排查:
- 检查电源:用万用表测量VCC和GND之间是否为3.3V
- 检查接线:确认所有引脚连接正确,特别是SCL、SDA、DC、RES、CS这5根线
- 检查复位:RES引脚要先拉低200ms再拉高,这个时序很重要
- 检查SPI配置:确认SPI时钟频率不要太高,OLED屏一般支持到10MHz左右
5.2 显示内容错乱
如果屏幕能亮但显示内容不对:
- 检查DC引脚:发送命令时DC要拉低,发送数据时DC要拉高
- 检查SPI模式:SSD1306通常使用模式0或模式3,确认
SPI_CK_PL_HIGH_PH_2EDGE是否正确 - 检查初始化命令:确保所有初始化命令都正确发送
- 检查显存更新:修改显存后要调用
OLED_Refresh()才能更新到屏幕
5.3 显示有残影或闪烁
- 调整对比度:修改初始化命令中的对比度值(0xCF)
- 检查电源稳定性:OLED对电源比较敏感,确保3.3V稳定
- 调整刷新频率:降低刷新频率或增加延时
5.4 性能优化建议
- 减少全屏刷新:只刷新变化的部分,不要每次都全屏刷新
- 使用局部更新:SSD1306支持设置显示区域,可以只更新特定区域
- 优化SPI速度:在保证稳定的前提下提高SPI时钟频率
- 使用DMA传输:如果需要大量数据传输,可以考虑使用SPI的DMA功能
6. 实际应用扩展
移植成功后,你可以在实际项目中使用这个OLED驱动了。这里分享几个我在项目中常用的技巧:
显示实时数据:比如显示传感器数据、系统状态等
// 显示温度数据 void show_temperature(float temp) { char buffer[20]; sprintf(buffer, "Temp: %.1fC", temp); OLED_ShowString(0, 0, buffer, 16, 1); OLED_Refresh(); }制作简单菜单:通过按键切换显示内容
typedef enum { MENU_MAIN, MENU_SETTINGS, MENU_INFO } menu_state_t; menu_state_t current_menu = MENU_MAIN; void update_display(void) { OLED_Clear(); switch(current_menu) { case MENU_MAIN: OLED_ShowString(0, 0, "Main Menu", 16, 1); OLED_ShowString(0, 20, "1. Settings", 16, 1); OLED_ShowString(0, 40, "2. Info", 16, 1); break; case MENU_SETTINGS: // 显示设置菜单 break; case MENU_INFO: // 显示信息 break; } OLED_Refresh(); }显示进度条:用于显示进度或电量
void draw_progress_bar(u8 x, u8 y, u8 width, u8 height, u8 progress) { // 画边框 OLED_DrawLine(x, y, x + width, y, 1); OLED_DrawLine(x, y + height, x + width, y + height, 1); OLED_DrawLine(x, y, x, y + height, 1); OLED_DrawLine(x + width, y, x + width, y + height, 1); // 填充进度 u8 fill_width = (width - 2) * progress / 100; for(u8 i = 0; i < height - 2; i++) { OLED_DrawLine(x + 1, y + 1 + i, x + 1 + fill_width, y + 1 + i, 1); } OLED_Refresh(); }移植过程中最常遇到的问题就是引脚配置不对和SPI时序问题。如果遇到问题,建议先用逻辑分析仪或示波器抓一下SPI波形,看看时钟、数据、片选这些信号是否正常。
还有一个坑要注意:GD32的SPI库函数和STM32的有些差异,特别是时钟配置部分。如果发现通信不正常,可以尝试调整SPI的预分频值,降低通信速度试试。
好了,以上就是GD32VW553驱动0.96寸OLED屏的完整移植过程。按照这个步骤来,应该能顺利点亮屏幕。在实际项目中,你可以根据需要添加更多显示功能,比如图表、动画等。