【STM32F103笔记】9、I2C通信——玩转OLED(OLED命令详解)

I2C通信——玩转OLED(OLED命令详解)

这一篇给大家介绍一下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命令字节的分析,给出初始化流程如下:

关闭显示
设置通道数=63
设置时钟及分频比为0xF0
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值