文章基于适用于STM32F4系列,作者使用STM32F401CCU6开发板。
本文章基于此系列和开发板展开讨论。
本系列以SSD1306为主控芯片的I2C接口的0.96寸OLED屏幕为例介绍
内容较多,分节进行
链接
需求分析
本节主要是最基础的I2C实现和数据发送
听说STM32的硬件I2C容易在中断的影响下出现问题
为了更方便的换用不同管脚和作为库文件移植,
本系列使用软件I2C,即用GPIO模拟I2C时序
本节需求
- 使用GPIO模拟I2C时序
- 数据与命令的发送
- OLED的初始化
软件I2C实现
流程
- GPIO初始化
- 模拟起始与终止信号
- 模拟数据信号与应答
GPIO初始化
与外设通讯时一般都使用开漏输出的输出模式(不管是输入还是输出)
这是因为开漏输出有线与的功能
即数据线上的电平只有在单片机和外设都输出高电平时才为高
而且可以单片机与外设一个为高电平,一个为低电平而不引发短路(此时数据线为低电平)
有因为I2C的数据线和时钟线的默认电平为高电平
GPIO的配置在之前介绍过了,传送门
这是本文使用的初始化函数和相关头文件定义
头文件(OLED.H)
#define GPIO_group GPIOA // I2C的GPIO组号
#define OLED_SCL GPIO_Pin_0 // I2C时钟的GPIO端口号
#define OLED_SDA GPIO_Pin_1 // I2C时钟的GPIO端口号
#define RCC_AHB1Periph RCC_AHB1Periph_GPIOA //所用组号的时钟
C文件(OLED.C)
//初始化GPIO
void OLED_GPIO_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct; //声明结构体
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph, ENABLE); //打开时钟
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT; //输出模式
GPIO_InitStruct.GPIO_OType = GPIO_OType_OD; //开漏输出
GPIO_InitStruct.GPIO_Pin = OLED_SCL | OLED_SDA; // SDA和SCL线
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; //快速模式
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_Init(GPIO_group, &GPIO_InitStruct); //初始化
GPIO_SetBits(GPIO_group, OLED_SCL | OLED_SDA); //设置为默认为高电平
}
起始信号
在I2C协议的文章中说过
I2C的起始信号是SCl为高电平时,SDA产生下降沿
因此这是起始信号的代码
头文件(OLED.H)
#define OLED_Write_SCL(x) GPIO_WriteBit(GPIO_group, OLED_SCL, x)
#define OLED_Write_SDA(x) GPIO_WriteBit(GPIO_group, OLED_SDA, x)
宏定义就是一个替换,这个(x)就是将后面出现x替换为传入的量,这就是写SDA和SCl的高低电平用
C文件(OLED.C)
__STATIC_INLINE void I2C_Start(void)
{
OLED_Write_SDA(Bit_SET);
OLED_Write_SCL(Bit_SET); //确保SCL与SDA为高电平
// Delay_us();//这里可以加延迟,保证信号稳定,是SDA高电平持续的延迟,编者的OLED模块不需要加
OLED_Write_SDA(Bit_RESET); //在SCL为高电平时SDA产生下降沿
// Delay_us();//这里可以加延迟,保证信号稳定,是SDA与SCL下降沿之间的延迟,编者的OLED模块不需要加
OLED_Write_SCL(Bit_RESET); //将SCL拉低,准备接受数据
}
函数声明前的
__STATIC_INLINE
是C++的一个关键字,意思是将这个函数作为内联函数
一句话就是以空间换时间,加速代码运行的
另外:单片机的两句命令之间会有一个固有的延迟,为(1/系统频率)(s)
终止信号
I2C的起始信号是SCl为高电平时,SDA产生上升沿
__STATIC_INLINE void I2C_End(void)
{ //从发送数据到结束部分,默认SCL为低电平
OLED_Write_SDA(Bit_RESET); //将SDA拉低
OLED_Write_SCL(Bit_SET);
// Delay_us();//这里可以加延迟,保证信号稳定,是SDA和SCL上升沿之间的延迟,编者的OLED模块不需要加
OLED_Write_SDA(Bit_SET);
// Delay_us();//这里可以加延迟,保证信号稳定,是SDA高电平持续的延迟,编者的OLED模块不需要加
}
数据信号与应答
数据是以8bit为1组从高位到低位发送的
void I2C_SendByte(u8 dat)
{
u8 zj = dat; //不能更改dat
for (int i = 7; i >= 0; i--) // 8次就行,怎么循环随意
{ //进入发送数据默认SCL为低电平
if (zj & 0x80) //判断最高位
{
OLED_Write_SDA(Bit_SET);
}
else
{
OLED_Write_SDA(Bit_RESET);
}
OLED_Write_SCL(Bit_SET); // SCL拉高,让外设读取数据
OLED_Write_SCL(Bit_RESET); //准备读取下个数据
zj <<= 1; //向左移位,让数据从高到低发送
}
OLED_Write_SCL(Bit_SET); //第九个周期,应答位
//可以加验证的代码,一般不需要验证
OLED_Write_SCL(Bit_RESET);
}
千万别忘了应答信号,可以不读取SDA的应答状态,但不能没有应答的时钟
到了这里,软件I2C发送数据的内容已经完成了,下面是针对SSD1306的命令部分了
数据与命令的发送
流程
- 发送起始信号
- 发送地址
- 选择命令或数据
- 发送命令或数据
- 发送结束信号
地址
本OLED模块的地址可以选择,改变这个电阻的链接位置即可
现在选择的地址是
0x78
命令和数据的选择
在发送了地址之后,需要选择接下来发送的8bit是命令还是数据
如果是命令需要发送
0x00
如果是命令需要发送
0x40
命令的发送
C文件(OLED.C)
//写入控制命令的开始 写完数据后加结束命令
void OLED_Write_Ctrl_Start(void)
{
I2C_Start();
I2C_SendByte(0x78);
I2C_SendByte(0x00);
}
例子
OLED_Write_Ctrl_Start();
I2C_SendByte(0xff);
I2C_SendByte(0x0f);
I2C_End();
注意:
1.调用开始发送命令函数后可以发送任意数量的命令(一条是8bit)
2.当需要发送数据时只需要调用开始发送数据的函数即可
3.当结束发送时一定要调用结束函数
数据的发送
C文件(OLED.C)
//写入控制命令的开始 写完数据后加结束命令
//写入数据的开始 写完数据后加结束命令
void OLED_Write_Data_Start(void)
{
I2C_Start();
I2C_SendByte(0x78);
I2C_SendByte(0x40);
}
例子
OLED_Write_Data_Start();
I2C_SendByte(0xff);
I2C_SendByte(0x0f);
I2C_End();
注意:
1.调用开始发送数据函数后可以发送任意数量的数据(一条是8bit)
2.当需要发送命令时只需要调用开始发送命令的函数即可
3.当结束发送时一定要调用结束函数
OLED的初始化
oled初始化没什么需要多说的,大体分为2步
- OLED初始化设置
- 清屏
OLED初始化设置
首先上电后需要一段时间的延迟,几毫秒就行,不需要特别精确
之后是一堆有关OLED的设置,基本上不需要要修改
C文件(OLED.C)
// OLED的初始化
void OLED_Init(void)
{
for (int i = 0; i < 1000; i++) //上电延时
{
for (int j = 0; j < 1000; j++)
;
}
OLED_GPIO_Init(); //初始化GPIO
OLED_Write_Ctrl_Start(); //开始写入初始化命令
I2C_SendByte(0xAE); //关闭显示
I2C_SendByte(0xD5); //设置显示时钟分频比/振荡器频率
I2C_SendByte(0x80);
I2C_SendByte(0xA8); //设置多路复用率
I2C_SendByte(0x3F);
I2C_SendByte(0xD3); //设置显示偏移
I2C_SendByte(0x00);
I2C_SendByte(0x40); //设置显示开始行
I2C_SendByte(0xA1); //设置左右方向,0xA1正常 0xA0左右反置
I2C_SendByte(0xC8); //设置上下方向,0xC8正常 0xC0上下反置
I2C_SendByte(0xDA); //设置COM引脚硬件配置
I2C_SendByte(0x12);
I2C_SendByte(0x81); //设置对比度控制
I2C_SendByte(0xCF);
I2C_SendByte(0xD9); //设置预充电周期
I2C_SendByte(0xF1);
I2C_SendByte(0xDB); //设置VCOMH取消选择级别
I2C_SendByte(0x30);
I2C_SendByte(0xA4); //设置整个显示打开/关闭
I2C_SendByte(0xA6); //设置正常/倒转显示
I2C_SendByte(0x8D); //设置充电泵
I2C_SendByte(0x14);
I2C_SendByte(0x2E); //关闭滚动
I2C_SendByte(0xAF); //开启显示
I2C_End();
OLED_Clear();
}
之后是清屏,本质上是将所有的列和页设置为0x00
C文件(OLED.C)
//清屏
void OLED_Clear(void)
{
OLED_Write_Ctrl_Start();
I2C_SendByte(0x20); //设置寻址模式 (0x00/0x01/0x02)
I2C_SendByte(0x00); //水平寻址模式
I2C_SendByte(0x21); //设置列地址
I2C_SendByte(0x00);
I2C_SendByte(0x7f);
I2C_SendByte(0x22); //设置页地址
I2C_SendByte(0x00);
I2C_SendByte(0x07);
I2C_End();
OLED_Write_Data_Start();
for (int i = 0; i < 8; i++) // 0-7页
for (int j = 0; j < 128; j++) // 0-127列
I2C_SendByte(0x00);
I2C_End();
}
这里使用的水平寻址模式,在之前文章中说过,传送门
可以将清屏函数中的发送的数据改为0xFF
,这样如果设置成功后则屏幕全部点亮,做为测试
成品
链接:百度网盘
提取码:ierk