当前位置: 首页 > news >正文

天空星STM32F407驱动0.96寸SSD1306 OLED屏:软件SPI与硬件SPI移植实战

天空星STM32F407驱动0.96寸SSD1306 OLED屏:软件SPI与硬件SPI移植实战

最近在做一个需要显示信息的小项目,手头正好有一块0.96寸的OLED屏,型号是SSD1306,通信接口是SPI。我用的主控板是天空星的STM32F407开发板。网上找的驱动代码通常都是软件模拟SPI的,虽然能用,但刷新速度慢,还占用CPU时间。我就琢磨着能不能改成硬件SPI来提升性能。

折腾了一番,总算把两种方式都调通了。今天我就把从拿到屏幕资料开始,到分别用软件SPI和硬件SPI成功驱动的完整过程,一步步分享出来。如果你是STM32的初学者,跟着做一遍,不仅能点亮屏幕,还能彻底搞懂SPI通信的两种实现方式,以后用起来就得心应手了。

1. 准备工作:认识你的屏幕和资料

工欲善其事,必先利其器。在动手写代码之前,咱们先得把屏幕的基本情况和手头的资料搞清楚。

我用的这块屏是中景园电子的0.96寸OLED,驱动芯片是SSD1306,分辨率是128x64像素。它通过SPI接口通信,总共有7个引脚(2.54mm间距的排针),工作电压是3.3V。

注意:不同厂家、不同批次的屏幕,初始化指令序列可能略有差异。所以,一定要使用屏幕卖家提供的配套资料,里面的例程代码是最可靠的。

卖家提供的资料里,最关键的是一个完整的工程例程。这个例程通常已经在一个具体的单片机平台(比如STM32F103)上验证通过了。我们的任务,就是把这个例程“搬家”到我们的天空星STM32F407开发板上。

这个“搬家”过程,专业点说就叫“移植”。核心工作就两件事:

  1. 改引脚:把例程里控制屏幕的GPIO引脚,改成我们开发板上实际连接的引脚。
  2. 改时序:调整代码中控制信号延时的时间,以适应不同主频的MCU。

2. 基础移植:软件模拟SPI

厂家提供的例程,默认基本都是用软件模拟SPI。什么意思呢?就是单片机并没有使用它内部那个专门的SPI硬件电路,而是用普通的GPIO引脚,通过程序代码控制它们高低电平的变化,来“模仿”出SPI通信的时序波形。

这种方法的好处是灵活,随便找几个IO口就能用,不挑引脚。缺点是速度慢,而且CPU得一直忙着“模仿”时序,干不了别的活。但对于初学者理解和点亮屏幕来说,这是最直接的方法。

2.1 获取并导入源码

首先,把资料包里的OLED文件夹整个复制到你自己的STM32工程目录下。然后在你自己的工程管理界面(比如Keil的Project窗口),把这些.c.h文件添加进来。

刚添加进来,编译肯定会报错,别慌,这很正常。第一个常见的错误是数据类型定义问题。厂家代码里用了u8u16这类缩写,我们需要在oled.h文件的开头,告诉编译器这些是什么。

oled.h文件中添加以下宏定义:

#ifndef u8 #define u8 uint8_t #endif #ifndef u16 #define u16 uint16_t #endif #ifndef u32 #define u32 uint32_t #endif

这样,代码里的u8就会被替换成uint8_t(无符号8位整数),以此类推,错误就消除了。

2.2 修改引脚配置

接下来是最关键的一步:根据你的实际接线,修改控制引脚的宏定义。

屏幕有7个引脚,我们需要在oled.h文件中找到引脚定义的地方,把它们改成我们开发板上的连接。假设我的接线如下:

  • SCL (时钟线)-> PA5
  • SDA (数据线)-> PA7
  • RES (复位线)-> PA3
  • DC (数据/命令选择线)-> PA2
  • CS (片选线)-> PA4

那么就需要把oled.h中对应的宏定义修改成下面这样:

//-----------------OLED端口定义---------------- /* D0(SCK) = PA5 D1(MOSI) = PA7 RES = PA3 DC = PA2 CS = PA4 */ #define OLED_SCL_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_5)//SCL #define OLED_SCL_Set() GPIO_SetBits(GPIOA,GPIO_Pin_5) #define OLED_SDA_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_7)//SDA #define OLED_SDA_Set() GPIO_SetBits(GPIOA,GPIO_Pin_7) #define OLED_RES_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_3)//RES #define OLED_RES_Set() GPIO_SetBits(GPIOA,GPIO_Pin_3) #define OLED_DC_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_2)//DC #define OLED_DC_Set() GPIO_SetBits(GPIOA,GPIO_Pin_2) #define OLED_CS_Clr() GPIO_ResetBits(GPIOA,GPIO_Pin_4)//CS #define OLED_CS_Set() GPIO_SetBits(GPIOA,GPIO_Pin_4)

每一对ClrSet的宏,就对应控制一个引脚输出低电平和高电平。

2.3 修改初始化函数

引脚定义改好了,还得初始化这些引脚。我们需要修改oled.c文件中的OLED_Init()函数。这个函数不光要初始化GPIO,还要发送一连串的指令给SSD1306芯片,完成屏幕的初始设置(比如对比度、扫描方向、显示模式等)。

找到OLED_Init()函数,将其替换为以下内容。这段代码做了两件事:

  1. 初始化我们上面定义的5个GPIO引脚(PA2, PA3, PA4, PA5, PA7)为推挽输出模式。
  2. 执行屏幕硬复位(拉低再拉高RESET引脚),然后发送厂家资料里提供的那一长串初始化命令。
//OLED的初始化 void OLED_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA,ENABLE);//使能PORTA时钟 //GPIO初始化设置 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;//普通输出模式 GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;//推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz;//100MHz GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP;//上拉 GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化 OLED_RES_Clr(); //复位OLED delay_ms(200); OLED_RES_Set(); //以下是SSD1306初始化命令序列 OLED_WR_Byte(0xAE,OLED_CMD);//关闭显示 OLED_WR_Byte(0x00,OLED_CMD);//设置列地址低4位 OLED_WR_Byte(0x10,OLED_CMD);//设置列地址高4位 OLED_WR_Byte(0x40,OLED_CMD);//设置显示起始行 OLED_WR_Byte(0x81,OLED_CMD);//设置对比度命令 OLED_WR_Byte(0xCF,OLED_CMD);//对比度值 OLED_WR_Byte(0xA1,OLED_CMD);//设置段重映射,0xA1正常,0xA0左右反置 OLED_WR_Byte(0xC8,OLED_CMD);//设置COM扫描方向,0xC8正常,0xC0上下反置 // ... (后续命令省略,请务必使用资料包中的完整序列) OLED_Clear(); //清屏 OLED_WR_Byte(0xAF,OLED_CMD);//开启显示 }

重要提示:上面代码中的初始化命令序列我做了省略。在实际移植时,必须原封不动地使用你资料包里oled.c文件中的那一长串OLED_WR_Byte命令,一个都不要改,也不要漏,否则屏幕可能显示异常。

至此,软件SPI的移植就完成了。你可以编译下载,看看屏幕是否点亮。

3. 进阶优化:硬件SPI驱动

软件SPI虽然简单,但就像用脚弹钢琴,能出声但效率太低。STM32F407内部有强大的硬件SPI外设,我们得把它用起来。

硬件SPI和软件模拟的本质区别在于:通信的时钟信号、数据移位等时序,全部由芯片内部的专用硬件电路自动完成,CPU只需要把要发送的数据扔到发送寄存器,或者从接收寄存器里读数据就行了。速度快,不占用CPU时间,通信也更稳定。

3.1 硬件SPI引脚选择

硬件SPI不是随便哪个引脚都能用的。STM32的SPI外设对应着固定的几组引脚。我们需要查阅STM32F407VET6的数据手册,找到SPI1、SPI2等外设对应的引脚。

以SPI1为例,它的主引脚在PA5(SCK)、PA6(MISO)、PA7(MOSI)。我们的屏幕只需要发送数据(MOSI)和时钟(SCK),并且我们用软件控制片选(CS),所以PA5和PA7正好可以用作硬件SPI1。

其他控制线(RES、DC、CS)不参与SPI时序,所以仍然可以用普通的GPIO,我们继续使用PA2、PA3、PA4。

3.2 修改初始化函数(硬件SPI版)

硬件SPI的初始化要复杂一些,因为除了配置GPIO,还要配置SPI外设本身。

我们需要重写OLED_Init()函数。这次,我们要做三件事:

  1. 初始化用作普通GPIO的RES、DC、CS引脚(PA2, PA3, PA4)。
  2. 初始化并配置SPI1的时钟线SCK(PA5)和数据线MOSI(PA7),将它们设置为复用功能(AF)
  3. 配置SPI1的工作模式、速率、数据格式等。

下面是修改后的OLED_Init()函数:

void OLED_Init(void) { GPIO_InitTypeDef GPIO_InitStructure1; GPIO_InitTypeDef GPIO_InitStructure2; SPI_InitTypeDef SPI_InitStructure; // 1. 开启GPIOA和SPI1的时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); // 2. 配置PA5和PA7为SPI1的复用功能引脚 GPIO_PinAFConfig(GPIOA, GPIO_PinSource5, GPIO_AF_SPI1); // PA5 -> SPI1_SCK GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_SPI1); // PA7 -> SPI1_MOSI GPIO_InitStructure1.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure1.GPIO_Mode = GPIO_Mode_AF; // 复用模式 GPIO_InitStructure1.GPIO_OType = GPIO_OType_PP; // 推挽输出 GPIO_InitStructure1.GPIO_Speed = GPIO_Speed_100MHz; GPIO_InitStructure1.GPIO_PuPd = GPIO_PuPd_UP; // 上拉 GPIO_Init(GPIOA, &GPIO_InitStructure1); // 3. 配置RES, DC, CS为普通输出引脚 GPIO_InitStructure2.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4; GPIO_InitStructure2.GPIO_Mode = GPIO_Mode_OUT; // 普通输出模式 GPIO_InitStructure2.GPIO_OType = GPIO_OType_PP; // 推挽输出 GPIO_InitStructure2.GPIO_Speed = GPIO_Speed_100MHz; GPIO_InitStructure2.GPIO_PuPd = GPIO_PuPd_UP; // 上拉 GPIO_Init(GPIOA, &GPIO_InitStructure2); // 初始化后置高电平 GPIO_SetBits(GPIOA, GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4); // 4. 配置SPI1参数 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 全双工 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主机模式 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8位数据 SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; // 时钟极性:空闲时为高 SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; // 时钟相位:第二个边沿采样 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 软件控制NSS(片选) SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 波特率预分频,决定SPI时钟速度 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 高位在前 SPI_InitStructure.SPI_CRCPolynomial = 7; // CRC多项式,这里用不上,但需要设置 SPI_Init(SPI1, &SPI_InitStructure); // 5. 使能SPI1 SPI_Cmd(SPI1, ENABLE); // 6. 屏幕硬件复位和初始化命令序列(与软件SPI版本完全相同) OLED_RES_Clr(); delay_ms(200); OLED_RES_Set(); OLED_WR_Byte(0xAE,OLED_CMD); OLED_WR_Byte(0x00,OLED_CMD); // ... 后续初始化命令必须完整保留 ... OLED_Clear(); OLED_WR_Byte(0xAF,OLED_CMD); }

关键点解释

  • SPI_CPOLSPI_CPHA:这是SPI通信的模式,决定了时钟空闲时的电平和数据采样的边沿。SSD1306通常工作在模式0或模式3。我这里是CPOL=High, CPHA=2Edge,对应模式3。如果你的屏幕不亮,可以尝试改成CPOL=Low, CPHA=1Edge(模式0)。
  • SPI_BaudRatePrescaler:SPI时钟分频器。_4表示SPI时钟=系统时钟/4。如果屏幕通信不稳定(有雪花点或乱码),可以尝试把这个值调大,比如_8_16,降低通信速度。

3.3 修改数据发送函数

软件SPI版本的数据发送函数OLED_WR_Byte,内部是用for循环和GPIO_SetBits/ResetBits来模拟时钟和数据变化的。现在用了硬件SPI,这个函数必须重写。

我们需要利用STM32标准库提供的SPI发送函数。找到oled.c中的OLED_WR_Byte函数,替换为以下内容:

void OLED_WR_Byte(u8 dat, u8 cmd) { // 1. 设置DC引脚:cmd=0表示发送命令,cmd=1表示发送数据 if(cmd) OLED_DC_Set(); // DC=1,数据 else OLED_DC_Clr(); // DC=0,命令 // 2. 拉低片选CS,选中OLED设备 OLED_CS_Clr(); // 3. 等待SPI发送缓冲区为空(表示可以发送新数据了) while(RESET == SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE)); // 4. 通过硬件SPI1发送一个字节数据 SPI_I2S_SendData(SPI1, dat); // 5. 等待SPI接收缓冲区非空(对于只发送不接收的情况,此步骤是为了确保数据发送完成) while(RESET == SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE)); SPI_I2S_ReceiveData(SPI1); // 读取接收到的数据(丢弃) // 6. 拉高片选CS,结束本次传输 OLED_CS_Set(); OLED_DC_Set(); // 将DC恢复为高电平(数据状态),这是一个好习惯 }

这个函数是硬件SPI驱动的核心。它利用SPI_I2S_SendData这个库函数,把数据dat交给SPI硬件去发送,CPU发完指令就可以去干别的事了,效率大大提升。

4. 验证与测试

无论你用软件SPI还是硬件SPI完成了移植,最后都需要写个简单的测试程序来验证。

在你的main.c文件中,可以这样写:

#include "board.h" #include "oled.h" int main(void) { // 开发板基础初始化(系统时钟、延时函数等) board_init(); // 初始化OLED屏幕 OLED_Init(); OLED_Clear(); // 清屏 while(1) { // 在屏幕不同位置,以不同字体大小显示“ABC” OLED_ShowString(0, 0, (uint8_t *)"Hello", 8, 1); OLED_ShowString(0, 16, (uint8_t *)"SkyStar", 16, 1); OLED_ShowString(0, 40, (uint8_t *)"OLED Test", 12, 1); // 更新显示到屏幕 OLED_Refresh(); // 延时一段时间 delay_ms(1000); OLED_Clear(); // 清屏,准备下一次显示 delay_ms(500); } }

编译程序,下载到天空星开发板,上电。如果一切顺利,你应该能看到屏幕上清晰地显示出“Hello”、“SkyStar”和“OLED Test”的字样。

5. 踩坑心得与总结

  1. 初始化命令序列是关键:屏幕不亮,80%的问题出在初始化命令不对。一定要用原厂资料里的完整序列,不要自己删减。
  2. SPI模式要匹配:硬件SPI不成功,首先检查CPOLCPHA的设置。SSD1306常用模式0或模式3,可以两个都试试。
  3. 时钟速度别太快:刚开始调试时,把SPI的波特率预分频设大一点(比如SPI_BaudRatePrescaler_32),先保证通信稳定,再尝试提高速度。
  4. 引脚复用别忘了:使用硬件SPI时,GPIO_PinAFConfig这个函数一定要调用,否则引脚还是普通的GPIO,无法产生SPI信号。
  5. 软件SPI的延时:如果坚持用软件SPI,发现屏幕显示乱码,可以尝试在OLED_WR_Byte函数里,每个CLK高低电平变化后加一个微秒级的delay_us(1),可能是CPU太快了,屏幕跟不上。

通过这次移植,你应该能深刻体会到软件模拟和硬件驱动的区别。对于OLED这种低速设备,软件SPI在项目初期验证想法时非常方便。但在实际产品中,尤其是需要频繁刷新显示或者CPU还有其它任务时,切换到硬件SPI是必然的选择。希望这篇教程能帮你打通任督二脉,下次遇到任何SPI设备,你都知道该怎么下手了。

http://www.cnnetsun.cn/news/1310960.html

相关文章:

  • yooasset OfflinePlayMode资源加载全链路解析
  • Windows11安装绕过TPM校验的实用技巧与常见问题解决
  • Arduino+esp8266+blinker物联网开发:从零搭建智能家居控制系统
  • 如何通过网盘直链解析技术实现文件下载效率提升
  • Gemma-3-12b-it高清图文问答展示:复杂场景理解与逻辑推理实例
  • 从“拳击沙包”到“信号反射”:阻抗匹配的工程直觉与实战解析
  • 立创Core Insight:基于STC32与ST7789屏的电脑硬件监控副屏DIY全解析
  • 新手福音:用快马ai生成带详细注释的ubuntu入门实战脚本
  • Qwen2.5-7B-Instruct效果展示:复杂SQL生成+自然语言转结构化JSON输出
  • eBPF 实用命令行工具详解
  • 基于ESP32-C3的智能卷帘电机闭环控制系统设计
  • LightTools VBA宏实战:如何一键提取杂散光分析数据(附完整代码)
  • wan2.1-vae多行业应用:教育课件插图、游戏原画草稿、建筑效果图生成
  • Phi-3 Forest Lab效果展示:对齐人类认知节奏的分步推理+自然停顿输出
  • Cosmos-Reason1-7B多场景:农业采摘机器人果实承重与夹持力推理
  • 美胸-年美-造相Z-Turbo效果展示:惊艳的半写实AI绘画作品集
  • YOLOv8鹰眼系统快速入门:无需深度学习基础,开箱即用
  • 从欧拉到RK4:IMU姿态解算中的数值积分方法选择与实践
  • Stable Yogi Leather-Dress-Collection 环境变量与配置文件详解:定制你的专属生成服务
  • 告别云端!GPT-OSS-20B本地部署指南:开源可控,16GB Mac就能跑
  • C# WinForm中动态调用外部EXE并实现多参数传递的实战指南
  • Phi-3-Mini-128K网络应用开发:基于Vue3构建智能问答管理后台
  • Oracle直方图实战:如何用DBMS_STATS优化SQL性能(附真实案例)
  • Prompt工程入门:从零开始设计高效AI提示词的完整指南(2024最新版)
  • Nano-Banana在软件测试中的应用:自动化测试脚本生成
  • 手把手教你用flv.js实现WebSocket直播流播放(附完整测试代码)
  • 从Windows转Mac必看!对照表式整理两系统快捷键差异(含M系列芯片适配问题)
  • 音频处理实战:如何用EQ参数整定优化你的音乐作品(避坑指南)
  • DeEAR语音情感识别实操手册:自定义阈值导出‘高唤醒预警’或‘低自然度告警’事件
  • 二阶系统动态特性揭秘:如何通过改变R/C值优化阶跃响应?