基于STM32G474的0.96寸OLED(SSD1306)显示屏驱动程序(4针脚I2C接口),支持硬件IIC/软件IIC,HAL库版。
这款驱动程序比较完善,可以实现 英文、整数、浮点数、汉字、图像、二进制数、十六进制数 等内容显示,可以画点、直线、矩形、圆、椭圆、三角形等,支持多种字体,差不多相当于一个简易版图形库了。
该程序是基于江协科技的代码二次修改的,原版程序是基于STM32F103的,且只支持软件I2C,我修改后支持硬件I2C,也可以修改宏定义改成使用软件I2C。
测试硬件为NUCLEO-G474RE开发板
关于OLED的驱动原理,以及驱动程序的使用教程可以看江协科技的视频:https://url.zeruns.tech/L7j6y
- STM32使用硬件I2C读取SHTC3温湿度传感器:https://blog.zeruns.tech/archives/692.html
- 移植好U8g2图形库的STM32F407标准库工程模板:https://blog.zeruns.tech/archives/722.html
- 基于STM32F1的0.96寸OLED显示屏驱动程序,支持硬件/软件I2C:https://blog.zeruns.tech/archives/769.html
电子/单片机技术交流群:820537762
效果图
I2C协议简介
I2C 通讯协议(Inter-Integrated Circuit)是由 Phiilps 公司开发的,由于它引脚少,硬件实现简单,可扩展性强,不需要 USART、CAN 等通讯协议的外部收发设备(那些电平转化芯片),现在被广泛地使用在系统内多个集成电路(IC)间的通讯。
I2C只有一跟数据总线 SDA(Serial Data Line),串行数据总线,只能一位一位的发送数据,属于串行通信,采用半双工通信。
半双工通信:可以实现双向的通信,但不能在两个方向上同时进行,必须轮流交替进行,其实也可以理解成一种可以切换方向的单工通信,同一时刻必须只能一个方向传输,只需一根数据线。
对于I2C通讯协议把它分为物理层和协议层物理层规定通讯系统中具有机械、电子功能部分的特性(硬件部分),确保原始数据在物理媒体的传输。协议层主要规定通讯逻辑,统一收发双方的数据打包、解包标准(软件层面)。
I2C物理层
I2C 通讯设备之间的常用连接方式
(1) 它是一个支持多设备的总线。“总线”指多个设备共用的信号线。在一个 I2C 通讯总线中,可连接多个 I2C 通讯设备,支持多个通讯主机及多个通讯从机。
(2) 一个 I2C 总线只使用两条总线线路,一条双向串行数据线SDA(Serial Data Line ),一条串行时钟线SCL(Serial Clock Line )。数据线即用来表示数据,时钟线用于数据收发同步
(3) 总线通过上拉电阻接到电源。当 I2C 设备空闲时会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
I2C通信时单片机GPIO口必须设置为开漏输出,否则可能会造成短路。
关于更多STM32的I2C相关信息和使用方法可以看这篇文章:https://url.zeruns.tech/JC0Ah
还有江协科技的STM32入门教程:https://www.bilibili.com/video/BV1th411z7sn?p=31
我这里就不详细讲解了。
使用说明
默认是使用硬件IIC的,用的I2C3,SCL是PA8,SDA是PC9。
硬件I2C
STM32CubeMX配置,找到你要用的I2C外设的引脚,并设置引脚功能为SCL和SDA,如下图所示是I2C3的SCL。
接着配置I2C外设,启用对应的I2C外设,速度模式设置为 Fast Mode Plus
,速度改成 1000,其他默认就行。
配置GPIO,上面设置完后会自动把那两个引脚配置为复用开漏输出模式,接着只需要把IO输出速度改成 Very High
,还有GPIO标签(User Label)定义分别改成I2C3_SCL
和I2C3_SDA
就行,如果是用的别的I2C也可以设置成别的值,代码对应处要修改一下。改完后点击生成代码。
OLED.c文件里,将 #define OLED_USE_SW_I2C
注释掉,将 #define OLED_USE_HW_I2C
取消注释,如果你用的是别的引脚作为I2C引脚,并且定义了别的名字那就将代码里的 I2C3_SCL
和 I2C3_SDA
也改一下。
软件I2C
STM32CubeMX配置,设置两个引脚作为I2C的SCL和SDA信号线,修改IO口的 User Lable
分别为I2C3_SCL
和I2C3_SDA
,如果改成别的需要到代码里修改一下,IO模式设置为开漏输出,默认输出电平高电平,上拉输出,速度设置到最高,如下图所示。改为后点击生成代码。
OLED.c文件里,将 #define OLED_USE_HW_I2C
注释掉,将 #define OLED_USE_SW_I2C
取消注释,如果你用的是别的引脚作为I2C引脚,并且定义了别的名字那就将代码里的 I2C3_SCL
和 I2C3_SDA
也改一下。
需要用的元件
- STM32开发板入门套件:https://u.jd.com/fQS0YAe
- STM32G474开发板:https://s.click.taobao.com/8OwQ8vt
- OLED模块:https://s.click.taobao.com/EF0Evwt
- 杜邦线:https://s.click.taobao.com/VMkDvwt
- 面包板:https://s.click.taobao.com/bhg8Txt
- DAPLink(可代替ST-Link,带虚拟串口):https://s.click.taobao.com/QVQ8Txt
江协科技的STM32入门套件:https://s.click.taobao.com/NTn9Txt
程序
完整工程下载地址:
百度网盘:链接: https://url.zeruns.tech/0CQJG 提取码: 0169
123网盘(不限速):https://www.123pan.com/s/2Y9Djv-O0cvH.html 提取码:vvDt
Gitee开源地址:https://gitee.com/zeruns/STM32-HAL-OLED-I2C
GitHub开源地址:https://github.com/zeruns/STM32G4-OLED-SSD1306-I2C-HAL
求点个Star
工程使用Keil5创建,用Vscode+EIDE开发,两个软件都可以打开此工程。
工程文件全部使用UTF-8编码,如果打开显示乱码需要修改编辑器编码为UTF-8。
主要文件 OLED.c:
/***************************************************************************************
* 本程序由江协科技创建并免费开源共享
* 你可以任意查看、使用和修改,并应用到自己的项目之中
* 程序版权归江协科技所有,任何人或组织不得将其据为己有
*
* 程序名称: 0.96寸OLED显示屏驱动程序(4针脚I2C接口)
* 程序创建时间: 2023.10.24
* 当前程序版本: V1.1
* 当前版本发布时间: 2023.12.8
*
* 江协科技官方网站: jiangxiekeji.com
* 江协科技官方淘宝店: jiangxiekeji.taobao.com
* 程序介绍及更新动态: jiangxiekeji.com/tutorial/oled.html
*
* 如果你发现程序中的漏洞或者笔误,可通过邮件向我们反馈:feedback@jiangxiekeji.com
* 发送邮件之前,你可以先到更新动态页面查看最新程序,如果此问题已经修改,则无需再发邮件
***************************************************************************************
*/
/*
* 本程序由zeruns二次修改
* 修改内容: 从标准库版改成HAL库版,增加支持硬件I2C,可通过修改宏定义来选择是否启用硬件I2C
* 修改日期: 2024.3.16
* 博客: https://blog.zeruns.tech
* B站主页: https://space.bilibili.com/8320520
*/
#include "main.h"
#include "OLED.h"
#include <string.h>
#include <math.h>
#include <stdio.h>
#include <stdarg.h>
// 如果用到中文,编译器附加选项需要加 --no-multibyte-chars (用AC6编译器的不用加)
/*
选择OLED驱动方式,默认使用硬件I2C。如果要用软件I2C就将硬件I2C那行的宏定义注释掉,将软件I2C那行的注释取消。
不能同时两个都同时取消注释!
在stm32cubemx中初始化时需要将SCL和SDA引脚的"user lable"分别设置为I2C3_SCL和I2C3_SDA。
*/
#define OLED_USE_HW_I2C // 硬件I2C
//#define OLED_USE_SW_I2C // 软件I2C
/*引脚定义,可在此处修改I2C通信引脚*/
#define OLED_SCL I2C3_SCL_Pin // SCL
#define OLED_SDA I2C3_SDA_Pin // SDA
#define OLED_SCL_GPIO_Port I2C3_SCL_GPIO_Port
#define OLED_SDA_GPIO_Port I2C3_SDA_GPIO_Port
/*STM32G474芯片的硬件I2C3: PA8 -- SCL; PC9 -- SDA */
#ifdef OLED_USE_HW_I2C
/*I2C接口,定义OLED屏使用哪个I2C接口*/
#define OLED_I2C hi2c3
extern I2C_HandleTypeDef hi2c3; //HAL库使用,指定硬件IIC接口
#endif
/*OLED从机地址*/
#define OLED_ADDRESS 0x3C << 1 // 0x3C是OLED的7位地址,左移1位最后位做读写位变成0x78
/*I2C超时时间*/
#define OLED_I2C_TIMEOUT 10
/*软件I2C用的延时时间,下面数值为170MHz主频要延时的值,如果你的主频不一样可以修改一下,100MHz以内的主频改成0就行*/
#define Delay_time 3
/**
* 数据存储格式:
* 纵向8点,高位在下,先从左到右,再从上到下
* 每一个Bit对应一个像素点
*
* B0 B0 B0 B0
* B1 B1 B1 B1
* B2 B2 B2 B2
* B3 B3 -------------> B3 B3 --
* B4 B4 B4 B4 |
* B5 B5 B5 B5 |
* B6 B6 B6 B6 |
* B7 B7 B7 B7 |
* |
* -----------------------------------
* |
* | B0 B0 B0 B0
* | B1 B1 B1 B1
* | B2 B2 B2 B2
* --> B3 B3 -------------> B3 B3
* B4 B4 B4 B4
* B5 B5 B5 B5
* B6 B6 B6 B6
* B7 B7 B7 B7
*
* 坐标轴定义:
* 左上角为(0, 0)点
* 横向向右为X轴,取值范围:0~127
* 纵向向下为Y轴,取值范围:0~63
*
* 0 X轴 127
* .------------------------------->
* 0 |
* |
* |
* |
* Y轴 |
* |
* |
* |
* 63 |
* v
*
*/
/*全局变量*********************/
/**
* OLED显存数组
* 所有的显示函数,都只是对此显存数组进行读写
* 随后调用OLED_Update函数或OLED_UpdateArea函数
* 才会将显存数组的数据发送到OLED硬件,进行显示
*/
uint8_t OLED_DisplayBuf[8][128];
/*********************全局变量*/
#ifdef OLED_USE_SW_I2C
/**
* @brief 向 OLED_SCL 写高低电平
* 根据 BitValue 的值,将 OLED_SCL 置高电平或低电平。
* @param BitValue 位值,0 或 1
*/
void OLED_W_SCL(uint8_t BitValue)
{
/*根据BitValue的值,将SCL置高电平或者低电平*/
HAL_GPIO_WritePin(OLED_SCL_GPIO_Port, OLED_SCL, (GPIO_PinState)BitValue);
/*如果单片机速度过快,可在此添加适量延时,以避免超出I2C通信的最大速度*/
for (volatile uint16_t i = 0; i < Delay_time; i++){
//for (uint16_t j = 0; j < 10; j++);
}
}
/**
* @brief OLED写SDA高低电平
* @param 要写入SDA的电平值,范围:0/1
* @return 无
* @note 当上层函数需要写SDA时,此函数会被调用
* 用户需要根据参数传入的值,将SDA置为高电平或者低电平
* 当参数传入0时,置SDA为低电平,当参数传入1时,置SDA为高电平
*/
void OLED_W_SDA(uint8_t BitValue)
{
/*根据BitValue的值,将SDA置高电平或者低电平*/
HAL_GPIO_WritePin(OLED_SDA_GPIO_Port, OLED_SDA, (GPIO_PinState)BitValue);
/*如果单片机速度过快,可在此添加适量延时,以避免超出I2C通信的最大速度*/
for (volatile uint16_t i = 0; i < Delay_time; i++){
//for (uint16_t j = 0; j < 10; j++);
}
}
#endif
/**
* @brief OLED引脚初始化
* @param 无
* @retval 无
* @note 当上层函数需要初始化时,此函数会被调用,
* 用户需要将SCL和SDA引脚初始化为开漏模式,并释放引脚
*/
void OLED_GPIO_Init(void)
{
uint32_t i, j;
/*在初始化前,加入适量延时,待OLED供电稳定*/
for (i = 0; i < 1000; i++) {
for (j = 0; j < 1000; j++)
;
}
#ifdef OLED_USE_SW_I2C
__HAL_RCC_GPIOC_CLK_ENABLE(); // 使能GPIOC时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能GPIOA时钟
GPIO_InitTypeDef GPIO_InitStruct = {
0}; // 定义结构体配置GPIO
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 设置GPIO模式为开漏输出模式
GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉电阻
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; // 设置GPIO速度为高速
GPIO_InitStruct.Pin = I2C3_SDA_Pin; // 设置引脚
HAL_GPIO_Init(I2C3_SDA_GPIO_Port, &GPIO_InitStruct);// 初始化GPIO
GPIO_InitStruct.Pin = I2C3_SCL_Pin;
HAL_GPIO_Init(I2C3_SCL_GPIO_Port, &GPIO_InitStruct);
/*释放SCL和SDA*/
OLED_W_SCL(1);
OLED_W_SDA(1);
#endif
}
// https://blog.zeruns.tech
/*通信协议*********************/
/**
* @brief I2C起始
* @param 无
* @return 无
*/
void OLED_I2C_Start(void)
{
#ifdef OLED_USE_SW_I2C
OLED_W_SDA(1); //释放SDA,确保SDA为高电平
OLED_W_SCL(1); //释放SCL,确保SCL为高电平
OLED_W_SDA(0); //在SCL高电平期间,拉低SDA,产生起始信号
OLED_W_SCL(0); //起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
#endif
}
/**
* @brief I2C终止
* @param 无
* @return 无
*/
void OLED_I2C_Stop(void)
{
#ifdef OLED_USE_SW_I2C
OLED_W_SDA(0); //拉低SDA,确保SDA为低电平
OLED_W_SCL(1); //释放SCL,使SCL呈现高电平
OLED_W_SDA(1); //在SCL高电平期间,释放SDA,产生终止信号
#endif
}
/**
* @brief I2C发送一个字节
* @param Byte 要发送的一个字节数据,范围:0x00~0xFF
* @return 无
*/
void OLED_I2C_SendByte(uint8_t Byte)
{
#ifdef OLED_USE_SW_I2C
uint8_t i;
/*循环8次,主机依次发送数据的每一位*/
for (i = 0; i < 8; i++)
{
/*使用掩码的方式取出Byte的指定一位数据并写入到SDA线*/
/*两个!的作用是,让所有非零的值变为1*/
OLED_W_SDA(!!(Byte & (0x80 >> i)));
OLED_W_SCL(1); //释放SCL,从机在SCL高电平期间读取SDA
OLED_W_SCL(0); //拉低SCL,主机开始发送下一位数据
}
OLED_W_SCL(1); //额外的一个时钟,不处理应答信号
OLED_W_SCL(0);
#endif
}
/**
* @brief OLED写命令
* @param Command 要写入的命令值,范围:0x00~0xFF
* @return 无
*/
void OLED_WriteCommand(uint8_t Command)
{
#ifdef OLED_USE_SW_I2C
OLED_I2C_Start(); // I2C起始
OLED_I2C_SendByte(0x78); //发送OLED的I2C从机地址
OLED_I2C_SendByte(0x00); //控制字节,给0x00,表示即将写命令
OLED_I2C_SendByte(Command); // 写入指定的命令
OLED_I2C_Stop(); // I2C终止
#elif defined(OLED_USE_HW_I2C)
uint8_t TxData[2] = {
0x00, Command};
HAL_I2C_Master_Transmit(&OLED_I2C, OLED_ADDRESS, (uint8_t*)TxData, 2, OLED_I2C_TIMEOUT);
#endif
}
/**
* @brief OLED写数据
* @param Data 要写入数据的起始地址
* @param Count 要写入数据的数量
* @return 无
*/
void OLED_WriteData(uint8_t *Data, uint8_t Count)
{
uint8_t i;
#ifdef OLED_USE_SW_I2C
OLED_I2C_Start(); // I2C起始
OLED_I2C_SendByte(0x78); //发送OLED的I2C从机地址
OLED_I2C_SendByte(0x40); // 控制字节,给0x40,表示即将写数据
/*循环Count次,进行连续的数据写入*/
for (i = 0; i < Count; i++) {
OLED_I2C_SendByte(Data[i]); // 依次发送Data的每一个数据
}
OLED_I2C_Stop(); // I2C终止
#elif defined(OLED_USE_HW_I2C)
uint8_t TxData[Count + 1]; // 分配一个新的数组,大小是Count + 1
TxData[0] = 0x40; // 起始字节
// 将Data指向的数据复制到TxData数组的剩余部分
for (i = 0; i < Count; i++) {
TxData[i + 1] = Data[i];
}
HAL_I2C_Master_Transmit(&OLED_I2C,