I2C通信——玩转OLED(OLED命令详解)
-
- I2C介绍
- OLED
-
- 1、SSD1306 I2C Slave Address
- 2、I2C通信格式
- 3、显存——GDDRAM
- 4、命令 Command
- 5、程序设计
- 6、测试
- 7、运行结果
- 完结撒花✿✿ヽ(°▽°)ノ✿
这一篇给大家介绍一下STM32的I2C总线通信方式,最后通过操作OLED来进行演示。
目前网上流传的OLED操作方式大同小异,基本都是同一个模子刻出来的,笔者在使用OLED店家给的测试程序时发现屏幕的刷新率不是很令人满意,每次刷新OLED时总会出现“闪屏”,影响OLED的显示效果,这对于后面想用STM32配合OLED做小游戏的笔者来说无法忍受,因此这一篇将通过分析SSD1306芯片手册(OLED控制芯片)来介绍更加高效的操作方式。
I2C介绍
STM32F103C8T6有2个I2C总线通信接口,其内部结构如下图所示:
具有以下特点:
- 支持主机模式(Master mode)与从机模式(Slave mode);
- 主机模式可以在SCL上发出时钟信号;在SDA上发出Start和Stop信号;
- 从机模式支持可编程地址,能够响应2个从机地址;能够接收Stop信号;
- 支持7位/10位地址模式;
- 标准传输速率100KHz,快速传输速率可达400KHz;
- 通过标志位对总线状态进行反馈和控制;
- 支持地址/数据通信完成中断和错误情况中断;
- 通信速率(SCL上时钟信号频率)可调;
- 支持DMA传输;
- 支持可配置PEC(数据包错误检查);
- 支持SMBus2.0。
好了,上面其实都是废话,主要是对STM32的I2C总线模块功能的简介,具体的功能介绍可以参考手册中的I2C functional description。
STM32程序开发正确的方式应该是确定所需要用到的外设,然后了解外设功能,确定具体要使用什么样的功能,最后对照寄存器结合库函数手册,确定外设初始化和控制的程序设计,最后编程调试。
I2C主机模式通信格式
在手册中,主机发送模式如下:
可知I2C主机模式下,发送格式为:
- 发出Start信号,检测EV5(I2C_SR1寄存器SB位,置1表示主机模式发出Start);
- 发送地址,等待应答信号(这里使能应答信号后,由硬件控制),检测EV6(I2C_SR1寄存器ADDR位,置1表示地址发送完毕),EV8_1可略过(因为之前没有发送过数据);
- 发送数据,等待应答信号,检测EV8(I2C_SR1寄存器TxE位,置1表示数据发送完毕);
- ……
- 发送Stop信号。
主机模式接收格式如下:
- 发出Start信号,检测EV5;
- 发送地址,等待应答信号,检测EV6;
- 检测EV7(I2C_SR1寄存器RxNE位,置1表示接收到数据),接收数据并发出应答信号;
- ……
- 最后一个字节数据接收后,不发出应答信号;
- 发送Stop信号。
I2C寄存器
下面对I2C总线的寄存器进行介绍,结合寄存器可以更进一步了解上面的通信格式:
1、Control register 1 (I2C_CR1)
I2C_CR1为I2C总线的控制寄存器1:
- Bit 15 SWRST:软件复位,置1使I2C总线进入复位状态;
- ……
- Bit 10 ACK:应答模式,置1使能应答;
- Bit 9 STOP:Stop信号,在主机模式中,置1在总线上发出Stop信号;
- Bit 8 START:Start信号,在主机模式中,置1在总线上发出Start信号;
- ……
- Bit 0 PE:使能位,置1使能I2C总线(对应的引脚将被复用为I2C总线引脚)。
2、Control register 2 (I2C_CR2)
I2C_CR2为I2C总线的控制寄存器2:
主要为DMA与中断设置,这里我们用不到:
- Bit 5:0 FREQ[5:0]:外设时钟设置,时钟频率为2~36,单位MHz;I2C总线挂载在APB1总线上,从第二篇可知,其时钟频率为36MHz,因此设置为36MHz即可。
3、Own address register 1 (I2C_OAR1)
I2C_OAR1为地址寄存器:
- ……
- Bit 7:1 ADD[7:1]:7位地址模式时地址;
- ……
4、Own address register 2 (I2C_OAR2)
I2C_OAR2为地址寄存器,用于双地址模式:
5、Data register (I2C_DR)
I2C_DR为数据寄存器,I2C总线以字节为单位传输数据,发送模式时将数据写入这个寄存器,接收模式时接收到的数据将会被复制到这个寄存器:
6、Status register 1 (I2C_SR1)
I2C_SR1状态寄存器1,这里主要关注几个和数据传输过程有关的寄存器:
- ……
- Bit 7 TxE:1表示数据寄存器为空,即发送完毕;
- Bit 6 RXNE:1表示数据寄存器不为空,即接收到数据;
- ……
- Bit 2 BTF:1表示字节传输完毕;
- Bit 1 ADDR:主机模式下,1表示地址发送完毕;
- Bit 0 SB:主机模式用,1表示Start信号发出。
7、Status register 2 (I2C_SR2)
I2C_SR2状态寄存器2:
- ……
- Bit 2 TRA:0表示接收到数据,1表示数据发送完毕;
- Bit 1 BUSY:1表示总线正在进行通信,总线忙;
- Bit 0 MSL:0表示从机模式,1表示主机模式。
8、Clock control register (I2C_CCR)
I2C_CCR时钟控制寄存器:
- Bit 15 F/S:模式选择,0-标准I2C模式,1-快速I2C模式;
- Bit 14 DUTY:快速模式下占空比,0-1:2,1-16:9,这个占空比用于适配通信设备,根据通信设备进行确定;
- Bit 11:0 CCR[11:0]:时钟控制。
9、TRISE register (I2C_TRISE)
I2C_TRISE上升时间控制寄存器。
I2C程序设计
同样,官方库中提供了用于I2C初始化的结构体,初始化函数如下:
/**
* @brief initialize i2c1
* @param None
* @return None
*/
void I2C1_Config(void)
{
GPIO_InitTypeDef PB67InitStruct;
I2C_InitTypeDef I2C1InitStruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
PB67InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
PB67InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
PB67InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &PB67InitStruct);
I2C1InitStruct.I2C_Mode = I2C_Mode_I2C;
I2C1InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
I2C1InitStruct.I2C_Ack = I2C_Ack_Enable;
I2C1InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C1InitStruct.I2C_ClockSpeed = 400000;
I2C_Init(I2C1, &I2C1InitStruct);
I2C_Cmd(I2C1, ENABLE);
}
这里首先将PB6(SCL)、PB7(SDA)引脚初始化为I2C通信引脚,然后对I2C初始化结构体赋值,调用I2C_Init()函数进行初始化设置,最后调用I2C_Cmd()函数使能I2C1。
这里给出I2C读写的函数,详细说明见注释,请对照上面的通信格式进行理解:
/**
* @brief i2c1 write data
* @param slave_addr - I2C slave device address
* @param reg_addr - I2C slave device register address
* @param length - Length of data to be written
* @param pdata - Data buf pointer
* @return 0 if success
*/
int8_t I2C1_Write(uint8_t slave_addr, uint8_t reg_addr, uint16_t length, uint8_t *pdata)
{
if(!length)
return 0;
// 等待I2C总线空闲
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
// 发出Start信号并检测EV5
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
// 发送地址并检测EV6
I2C_Send7bitAddress(I2C1, slave_addr, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
// 发送数据(寄存器地址或命令,一般来说,向I2C通信对应的设备发送数据,是对其某个寄存器发送数据,或者发送对应的命令及数据),并检测EV8
I2C_SendData(I2C1, reg_addr);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
// 继续发送数据,并检测EV8
while(length--)
{
I2C_SendData(I2C1, *pdata);
pdata++;
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
}
// 发送Stop信号
I2C_GenerateSTOP(I2C1, ENABLE);
return 0;
}
/**
* @brief i2c1 read data
* @param slave_addr - I2C slave device address
* @param reg_addr - I2C slave device register address
* @param length - Length of data to be read
* @param pdata - Data buf pointer
* @return 0 if success
*/
int8_t I2C1_Read(uint8_t slave_addr, uint8_t reg_addr, uint16_t length, uint8_t *pdata)
{
if(!length)
return 0;
// 等待I2C总线空闲
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
// 发出Start信号并检测EV5
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
// 发送地址并检测EV6
I2C_Send7bitAddress(I2C1, slave_addr, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
// 发送数据(寄存器地址或命令),并检测EV8
I2C_SendData(I2C1, reg_addr);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
// 发出Start信号并检测EV5
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
// 发送地址并检测EV6
I2C_Send7bitAddress(I2C1, slave_addr, I2C_Direction_Receiver);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
// 检测EV7,读取数据并发出应答信号
while(length--)
{
if(!length)
I2C_AcknowledgeConfig(I2C1, DISABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));
*pdata = I2C_ReceiveData(I2C1);
pdata++;
}
I2C_AcknowledgeConfig(I2C1, ENABLE);
// 发出Stop信号
I2C_GenerateSTOP(I2C1, ENABLE);
return 0;
}
注意这里的读写函数仅作为示范,具体的读写函数应该根据通信设备的协议要求进行编写。
至此,STM32的I2C总线通信已经完成,接下来就是对照OLED手册进行OLED程序设计了。
OLED
笔者用的是一块从箱子里翻出来不知道什么时候买的OLED(保护膜居然还没舍得揭掉),大小为128*64,查了查资料,找到了驱动芯片SSD1306的手册,因此,对OLED进行控制主要是和SSD1306这块芯片进行数据通信。
SSD1306芯片提供了4种通信方式:
- 6800并行通信;
- 8080并行通信;
- SPI接口;
- I2C接口。
笔者这块OLED将SSD1306芯片的I2C接口引出,因此通过I2C接口对其进行控制,下面根据SSD1306的手册对此进行介绍。
1、SSD1306 I2C Slave Address
根据电路,SA0(D/C#引脚接地)为0,因此,Slave Address取0b01111000,即0x78。
2、I2C通信格式
根据手册,SSD1306 I2C通信格式为:
- 发送Start信号;
- 发送地址(0x78);
- 发送Control Byte,即控制字节,用于告诉SSD1306后续字节是命令还是数据;
- 发送数据字节(这里的数据可以是命令,也可以是写入显存的数据);
- 依次类推,直到通信完成。
注意这里的是在一次I2C通信中,可以接收多个命令与数据,因此找到了提高刷新率的方法。
Control Byte:
b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 |
---|---|---|---|---|---|---|---|
Co | D/C | 0 | 0 | 0 | 0 | 0 | 0 |
- Co:清0表示后续只有数据字节,即后续数据都是存入GDDRAM的图像数据;
- D/C:Data/Command,清0表示后续为命令字节,置1表示后续为存入GDDRAM的图像数据。
因此:
- 若发送命令,则Control Byte为0x80;
- 若发送数据,则Control Byte为0x40。
3、显存——GDDRAM
GDDRAM全称Graphic Display Data RAM,为SSD1306内部的数据内存,SSD1306通过Common Driver驱动OLED显示,可以理解为OLED显示的图像跟随GDDRAM里的内容(当然也可以设置不跟随,用于刷新处理),下图为SSD1306驱动OLED显示屏的逻辑关系:
在上电初始化后,SSD1306默认设置为COM0驱动Row0,COM63驱动Row63(后续可以设置);COM驱动对应内部GDDRAM关系如下:
GDDRAM共分为8个Page,每个Page对应8个COM,每个COM驱动一行OLED显示,即共64行;而一个COM驱动内有128列(Segment),组成128*64OLED点阵驱动:
总结如下:
- 显存GDDRAM大小为128*64;
- SSD1306通过Common Driver驱动OLED每一行的显示;
- 控制上,每8个COM组成一个Page,共有8个Page,即64行;
- 每一个COM中有128个Segment,即128列;
- 通过设置COM与Page的映射关系,可以改变OLED的行刷新方向;
- 通过设置SEG与Column的映射关系,可以改变OLED的列刷新方向;
- 在一个Page中,数据刷新总是从低SEG刷新到高SEG;在一个SEG中,数据的低位在上,高位在下,即在一个SEG中数据总是从低COM刷新到高COM。
至此,我们弄清楚了GDDRAM里数据和OLED显示的关系。
4、命令 Command
这里按照SSD1306手册上的Command Table中的命令出现顺序进行介绍。
4.1 Fundamental Command 基础命令
4.1.1 Set Contrast Control (设置对比度)
格式 | 命令 | 数据 |
---|---|---|
- | 0x81 | 对比度(0~255) 默认值为0x7F(127) |
这个命令首先发送命令字节0x81,然后再发送对比度数据。
实际上是设置OLED的驱动电流,对比度设置越大,驱动电流越大,显示效果就越亮。
4.1.2 Entire Display ON (OLED显示跟随GDDRAM)
格式 | 命令 | 数据 |
---|---|---|
- | 0xA4:OLED显示跟随GDDRAM 0xA5:OLED显示固定,不跟随GDDRAM 默认为0xA4 |
无 |
这个命令只需发送命令字节0xA4或0xA5,可以设置OLED的显示内容是否跟随GDDRAM内容变化。
4.1.3 Set Normal/Inverse Display (设置反色)
格式 | 命令 | 数据 |
---|---|---|
- | 0xA6:正常显示,即GDDRAM中1表示显示,0表示不显示 0xA7:反转显示,即GDDRAM中0表示显示,1表示不显示 默认为0xA6 |
无 |
这个命令只需发送命令字节0xA6或0xA7,可以设置OLED的显示反色。
4.1.4 Set Display ON/OFF (开启/关闭显示)
格式 | 命令 | 数据 |
---|---|---|
- | 0xAE:关闭显示,进入睡眠模式 0xAF:开启显示 默认为0xAE |
无 |
这个命令只需发送命令字节0xAE或0xAF,可以设置OLED是否开启显示。
4.2 Scrolling Command 滚动设置
暂时不需要使用
4.3 Addressing Setting Command 寻址方式设置
4.3.1 Set Lower Column Start Address for Page Addressing Mode
格式 | 命令 | 数据 |
---|---|---|
- | 0x00~0x0F 默认为0x00 |
无 |
这个命令设置在页寻址(Page Addressing)模式下列起始地址的低4位。
这个命令仅在页寻址模式下有效。
4.3.2 Set Higher Column Start Address for Page Addressing Mode
格式 | 命令 | 数据 |
---|---|---|
- | 0x10~0x1F 默认为0x10 |
无 |
这个命令设置在页寻址(Page Addressing)模式下列起始地址的高4位,和上一命令一起指定页寻址模式下的起始列地址。
这个命令仅在页寻址模式下有效。
4.3.3 Set Memory Addressing Mode
格式 | 命令 | 数据 |
---|---|---|
- | 0x20 | 0x00:Horizontal Addressing Mode 行寻址模式 0x01:Vertical Addressing Mode 列寻址模式 0x02:Page Addressing Mode 页寻址模式 默认为0x02 |
这个命令首先发送0x20命令字节,然后发送寻址模式选择字节,各寻址模式如下:
页寻址模式:
水平/行寻址模式:
垂直/列寻址模式:
4.3.4 Set Column Address
格式 | 命令 | 数据1 | 数据2 |
---|---|---|---|
- | 0x21 | 0~127 默认为0 |
0~127 默认为127 |
这个命令首先发送0x21命令字节,然后发送2个数据字节用于设定列的起始和结束地址。
此命令仅在行/列寻址模式下有效。
4.3.5 Set Page Address
格式 | 命令 | 数据1 | 数据2 |
---|---|---|---|
- | 0x22 | 0~7 默认为0 |
0~7 默认为7 |
这个命令首先发送0x22命令字节,然后发送2个数据字节用于设定页的起始和结束地址。
此命令仅在行/列寻址模式下有效。
这个命令结合上一个命令,可以在显示区域设定一个窗口进行数据寻址刷新,如下图:
4.3.6 Set Page Start Address for Page Addressing Mode
格式 | 命令 | 数据 |
---|---|---|
- | 0xB0~0xB7 默认为0xB0 |
无 |
这个命令用于设置页寻址模式下的页起始地址。结合命令4.3.1和4.3.2,可以设置页寻址模式下一页的数据寻址刷新,如下图设置起始页为Page2,起始列为3:
4.4 Hardware Configuration (Panel resolution & layout related) Command 硬件设置
这里的命令主要是设置硬件映射关系等。
4.4.1 Set Display Start Line
格式 | 命令 | 数据 |
---|---|---|
- | 0x40~0x7F 对应0-63行 默认为0x40,第0行 |
无 |
这个命令设置显示的起始行。
4.4.2 Set Segment Re-map
格式 | 命令 | 数据 |
---|---|---|
- | 0xA0:Column 0 映射到 SEG0 0xA1:Column 127 映射到 SEG0 默认为0xA0 |
无 |
此命令设置列(Column)对应Segment的映射,即从左到右刷新数据还是从右到左刷新数据,需要根据实际显示情况进行设置,如果不知道怎么设置,可以先设置为默认值,根据显示情况进行调整。
4.4.3 Set Multiplex Ratio
格式 | 命令 | 数据 |
---|---|---|
- | 0xA8 | 0x10~0x3F 对应15+1 ~ 63+1 默认为63 |
这个命令设置通道数,应该选取默认63即可。
4.4.4 Set COM Output Scan Direction
格式 | 命令 | 数据 |
---|---|---|
- | 0xC0:从COM0扫描到COM N 0xC8:从COM N扫描到COM0 默认为0xC0 |
无 |
这个命令设置Common Driver的扫描方向,也就是OLED从上往下扫描还是从下往上扫描,需要根据具体的显示情况设置。
4.4.5 Set Display Offset
格式 | 命令 | 数据 |
---|---|---|
- | 0xD3 | 0~63 默认为0 |
这个命令设置COM的偏移值,一般设置为0,保证屏幕的完整显示。
4.4.6 Set COM Pins Hardware Configuration
这个命令可以用于更为复杂的屏幕显示,如使屏幕从中间开始刷新;这里保持默认设置即可。
4.5 Timing & Driving Scheme Setting Command 时序设置
4.5.1 Set Display Clock Divide Ratio/Oscillator Frequency
格式 | 命令 | 数据 |
---|---|---|
- | 0xDA | A[3:0]:设置时钟分频比 A[7:4]:设置时钟频率,值越大频率越高 |
这里为了让OLED能够更快地刷新,可以设置为0xF0,即分频比为1,频率最高。
4.5.2 Set Pre-charge Period
这个命令设定预充电时间,这里保持默认值0x22即可。
4.5.3 Set VCOMH Deselect Level
这里保持默认0x20即可。
4.5.4 NOP
格式 | 命令 | 数据 |
---|---|---|
- | 0xE3 | 无 |
空操作。
5、程序设计
5.1 I2C通信函数
根据第1节中的I2C通信格式图示,可以知道,在一次I2C通信中,SSD1306是可以接收多个命令和数据的,因此,为了方便扩展一次I2C通信,这里将I2C起始、发送命令/数据、I2C停止分别设置成函数:
I2C通信发起函数:
/**
* @brief OLED write mode initiates
* @param oled_addr - OLED slave address
* @return None
*/
static __INLINE void OLED_WriteInitiates(uint8_t oled_addr)
{
// 等待I2C总线空闲
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));
// 发出Start信号并检测EV5
I2C_GenerateSTART(I2C1, ENABLE);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
// 发送地址(0x78)并检测EV6
I2C_Send7bitAddress(I2C1, oled_addr, I2C_Direction_Transmitter);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
}
I2C写命令函数: 发送Control Byte
/**
* @brief OLED write mode data format
* @param ctrl_byte - 0x80 write cmd, 0x40 write only data
* @return None
*/
static __INLINE void OLED_WriteCtrlByte(uint8_t ctrl_byte)
{
// 发送Control Byte并检测EV8
I2C_SendData(I2C1, ctrl_byte);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
}
I2C写数据函数: 发送命令字节或者数据字节
/**
* @brief OLED write mode data format
* @param data - cmd or data
* @return None
*/
static __INLINE void OLED_WriteDataByte(uint8_t data)
{
// 发送数据并检测EV8
I2C_SendData(I2C1, data);
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
}
I2C通信结束函数:
/**
* @brief OLED write mode finish
* @param None
* @return None
*/
static __INLINE void OLED_WriteFinish(void)
{
// 发出Stop信号
I2C_GenerateSTOP(I2C1, ENABLE);
}
5.2 OLED初始化设置
根据对SSD1306命令字节的分析,给出初始化流程如下: