【STM32学习】I2C通信协议 | OLED屏

news2024/12/24 2:12:44

🐱作者:一只大喵咪1201
🐱专栏:《STM32学习》
🔥格言:你只管努力,剩下的交给时间!
图

今天需要将代码烧录到开发板中,本喵默认大家都会创建工程,以及进行基本的外设配置。

I2C通信协议 | OLED屏

  • 😽I2C协议
    • 🙈数据格式
    • 🙈I2C信号时序
    • 🙈I2C驱动代码
  • 😽OLED显示
    • 🙈SSD1306
    • 🙈 SSD1306的I2C总线数据格式
    • 🙈OLED的显示
      • OLED初始化
      • 显示字符
  • 😽源码及资料
  • 😽总结

😽I2C协议

I2C协议是一种通信协议,通常用来在主设备和从设备之间进行通信,本喵使用的主设备是STM32F103ZET6芯片的开发板,从设备使用的是SSD1306芯片驱动的OLED屏幕。

图
I2C在硬件上的接法如上图所示,主控芯片引出两条线SCLSDA线,在一条I2C总线上可以接很多I2C设备,我们还会放一个上拉电阻(放一个上拉电阻的原因以后我们再说)。

🙈数据格式

写操作:

图
如上图所示,白色背景表示"主→从",灰色背景表示"从→主",具体流程如下:

  • 主芯片要发出一个start信号,表示通信开始。
  • 然后发出一个字节的数据,包括设备地址(用来确定是往哪一个从芯片写数据),该地址有7个比特位以及方向(读/写,0表示写,1表示读),这里该位是0
  • 从设备回应(用来确定这个设备是否存在),如果存在就可以传输数据。
  • 主设备发送一个字节的数据给从设备,并等待回应。
  • 每传输一个字节数据,接收方要有一个回应信号(确定数据是否接受完成),然后再传输下一个数据。
  • 数据发送完之后,主芯片就会发送一个停止信号。

读操作:

图
如上图所示,白色背景表示"主→从",灰色背景表示"从→主",具体流程如下:

  • 主芯片要发出一个start信号,表示通信开始。
  • 然后发出一个字节的数据,包括设备地址(用来确定是往哪一个从芯片写数据),该地址有7个比特位以及方向(读/写,0表示写,1表示读),这里该位是1
  • 从设备回应(用来确定这个设备是否存在),然后就可以接收数据。
  • 从设备发送一个字节数据给主设备,并等待回应。
  • 主设备每接收一个字节数据,就要有一个回应信号(确定数据是否接受完成),然后再接收下一个数据。
  • 主设备认为数据接收完之后,就会发送一个停止信号。

上面的写操作和读操作,都是由主设备占据主动,无论是开始发送数据还是接收数据,从设备被动的根据方向位的值来配合主设备工作。

🙈I2C信号时序

图
如上图所示便是I2C信号的时序图,I2C协议中数据传输的单位是字节,也就是8位。但是要用到9个时钟,前面8个时钟用来传输8数据,第9个时钟用来传输应答信号,传输时,先传输最高位(MSB)。

  • SDA线上传输的数据必须在SCL线为高电平期间保持稳定,只能在SCL为低电平期间变化(由高到低或由低到高)。
  • 开始信号(S):SCL为高电平时,SDA由高电平向低电平跳变,开始传送数据。
  • 结束信号(P):SCL为高电平时,SDA由低电平向高电平跳变,结束传送数据。
  • 应答信号(ACK):接收方在接收到8位数据后,在第9个时钟周期,拉低SDA上的电平状态。

在一个字节传输完成,并且得到应答ACK信号以后,需要将SCL线上的电平状态拉低一段时间,为了给接收方充足的时间去处理数据,避免数据覆盖。

细节:

  • 主、从设备都可以通过SDA发送数据,肯定不能同时发送数据,怎么错开时间?

在9个时钟里,前8个时钟由主设备发送数据的话,第9个时钟就由从设备发送应答数据;前8个时钟由从设备发送数据的话,第9个时钟就由主设备发送应答数据。

  • 双方设备中,某个设备发送数据时,另一方怎样才能不影响SDA上的数据?

假设主设备正在给从设备发送数据,但是在某个时刻,从设备发生了故障或者误操作,导致连接双方的SDA线有了电势差,此时SDA线就导通了,可能产生严重的影响甚至烧坏芯片。

图
如上图所示,为了避免另一方对SDA线上的数据造成影响,需要让双方设备的SDA中有一个三极管,所以使用开极/开漏电路(三极管是开极,CMOS管是开漏,作用一样),并且使用上拉电阻将SDA线拉高。

  • 开漏输出模式正好符合上面的要求,所以使用I2C通信的时候,需要将主设备的SDA线和SCL线所在IO口设置成开漏输出模式。
  • 从设备也必须具有开漏输出的特性。

图
如上图所示是GPIO的输出电路,可以设置成推挽或者开漏输出模式,其中TTL肖特基触发器是打开的,所以IO口引脚的电平状态直接在输入数据寄存器中可以读到。

将输出设置为开漏输出模式时,输出驱动器中的P-MOS管就不会在导通了,只有N-MOS管在输出控制器输出低电平的时候会导通。

  • 输出控制器输出高电平时,IO引脚的电平状态由外部决定,由外部上拉电阻或者通信对端决定。
  • 输出控制器输出低电平时,IO口引脚接地,输出低电平。
  • 当开漏输出的IO控制器输出高电平时,相当于释放了该IO口的电平状态控制权。

所以当主设备A和从设备B都使用开漏输出模式控制SDA线的时候,SDA的真值表如下:

ABSDA
000
010
100
111
  • 通过真值表可以看到,SDA线上是不会存在电势差的,所以也不会导通。

所以接收方在接收数据之前,需要给SDA口输出高电平释放控制权(写1),此时SDA上的电平状态就完全由发送方决定,并且和IO口控制器输出的电平相一致。

而且双方都可以通过读取输入数据寄存器中的值来获取当前SDA线上的电平状态。


此时再看I2C通信中主设备向从设备写数据的过程:

启动信号发出后,前8个时钟clk

  • 从设备不能影响SDA线,所以不驱动N-MOS管,从设备IO口始终输出高电平,释放控制权。
  • 主设备决定数据,IO口变化SDA线电平状态,低电平时驱动N-MOS管,SDA线电平为低,高电平时不驱动N-MOS管,SDA线电平被外部上拉电阻拉高。

第9个时钟clk

  • 主设备不驱动N-MOS管,IO口输出高电平,释放SDA控制权。
  • 从设备决定数据,因为是应答信号,所以驱动N-MOS管,SDA线为低电平。
  • 在主设置经过8个clk后,需要先将SCL线电平拉低,同时给SDA写1,保持一定时间后再将SCL线拉高,读取SDA线的电平状态,如果变成低说明应答到来。
  • SCL拉低的目的是好让从设备改变SDA线电平状态,然后SCL保持高电,此时读到的SDA线电平才是真实的电平状态。
  • 为什么SCL也需要上拉呢?

在第9个时钟之后,如果有某一方需要更多的时间来处理数据,它可以一直驱动三极管把SCL拉低,也就是输出低电平。

SCL为低电平时候,大家都不应该使用I2C总线,只有当SCL从低电平变为高电平的时候,I2C总线才能被使用。

当它就绪后,就可以不再驱动三极管,这时上拉电阻把SCL变为高电平,其他设备就可以继续使用I2C总线了。

🙈I2C驱动代码

driver_i2c.h

#ifndef __DRIVER_I2C_H
#define __DRIVER_I2C_H

#include "stm32f1xx_hal.h"

/*********引脚定义**********/

#define SCL_PIN				GPIO_PIN_10
#define SDA_PIN				GPIO_PIN_11

#define SCL_PORT			GPIOF
#define SDA_PORT			GPIOF

/*********宏定义**********/

#define SCL_LOW			HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_RESET)
#define SCL_HIGH		HAL_GPIO_WritePin(SCL_PORT, SCL_PIN, GPIO_PIN_SET)

#define SDA_LOW			HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_RESET)
#define SDA_HIGH		HAL_GPIO_WritePin(SDA_PORT, SDA_PIN, GPIO_PIN_SET)
#define SDA_IN			HAL_GPIO_ReadPin(SDA_PORT, SDA_PIN)

/*********I2C引脚初始化**********/

void extern I2C_GPIO_ReInit(void);

/*********I2C驱动********/
extern void I2C_Start(void);
extern void I2C_Stop(void);
extern int I2C_GetAck(void);
extern void I2C_Ack(void);
extern void I2C_WriteByte(uint8_t data);
extern uint8_t I2C_ReadByte(uint8_t ack);

#endif /*__DRIVER_I2C_H*/

将用到的资源进行宏定义,像SCL线电平拉高拉低,SDA线电平拉高拉低等简单操作,同样通过宏来实现,比较复杂的操作就用函数实现,这里放的是函数声明具体的定义再driver_i2c.c中,下面本喵就讲解它们的实现。

I2C延时函数:

/*********I2C延时函数*********/
void I2C_Delay(uint32_t cnt)
{
	volatile uint32_t tmp = cnt;
	while(tmp--);
}

SCL线和SDA线上的电平状态需要保持一定的时间,HAL_Delay延时函数的单位是1ms,所以最短延时1ms,对于I2C通信来说,这个时间太长了,通信效率太低,所以本喵自己实现了一个用来I2C延时的函数,具体时间大家可以自己决定。

/*********I2C引脚初始化**********/
void I2C_GPIO_ReInit(void)
{
	GPIO_InitTypeDef GPIO_InitStruct = {0};//实例化IO口
	
	HAL_GPIO_DeInit(SCL_PORT,SCL_PIN);
	HAL_GPIO_DeInit(SDA_PORT,SDA_PIN);//恢复默认
	
	__HAL_RCC_GPIOF_CLK_ENABLE();//开启IO口时钟
	
	GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;//设置开漏输出模式
  	GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
	
	GPIO_InitStruct.Pin = SCL_PIN;//指定SCL引脚
	HAL_GPIO_Init(SCL_PORT,&GPIO_InitStruct);//初始化SCLIO口
	
	GPIO_InitStruct.Pin = SDA_PIN;//指定SDA引脚
	HAL_GPIO_Init(SDA_PORT,&GPIO_InitStruct);//初始化SDAIO口
}

上面代码是对SDA线和SCL线IO口引脚的初始化,必须设置成开漏输出模式,其他部分不解释,可以根据实际情况做修改。

开始信号:

/**********开始***********/
void I2C_Start(void)
{
	SCL_HIGH;//SCL线拉高
	SDA_HIGH;//SDA线拉高
	I2C_Delay(100);//保持
	
	SDA_LOW;//SDA线拉低
	I2C_Delay(100);//保持
}

先将SDA线和SCL线都拉高,维持一段时间后将SDA线拉低,再维持一段时间,此时就实现了SCL高电平期间,SDA由高电平变成了低电平,I2C通信开始。

停止信号:

/**********结束**********/
void I2C_Stop(void)
{
	SCL_HIGH;//SCL线拉高
	SDA_LOW;//SDA线拉低
	I2C_Delay(100);
	
	SDA_HIGH;//SDA线拉高
	I2C_Delay(100);
}

先将SCL线拉高和SDA线拉低,维持一段时间后再将SDA线拉高,再维持一段时间,此时就实现了SCL高电平期间,SDA由低电平变成了高电平,I2C通信结束。

获取应答信号:

/**********获取应答**********/
int I2C_GetAck(void)
{
	uint16_t i = 0;
	
	SCL_LOW;//SCL线拉低
	SDA_HIGH;//SDA线拉高
	I2C_Delay(100);
	
	SCL_HIGH;//SCL线拉高
	
	while(SDA_IN != 0)
	{
		//读取SDA状态一段时间
		i++;
		if(i == 1000)	
		{
			SCL_LOW;//SCL线拉低
			return -1;//仍然是1返回-1表示无应答信号
		}
	}
	SCL_LOW;//SCL线拉低
	return 0;//读到低电平返回0表示这是应答信号
}

主机获取应答信号时,先将SCL线拉低才能将SDA线拉高,然后维持一段时间再将SCL线拉高,释放SDA线控制权,再检测SDA线电平状态,检测一定时间后,如果SDA仍然是高电平,说明从机没有返回应答,返回-1,如果SDA变成低电平,说明从机返回了应答信号,返回0。

发送应答信号:

/***********发送应答**********/
void I2C_Ack(void)
{
	SCL_LOW;//SCL拉低
	SDA_LOW;//SDA拉低
	I2C_Delay(100);
	SCL_HIGH;//SCL拉高
	I2C_Delay(100);
}

先将SCL先和SDA线都拉低,维持一段时间后将SCL线拉高,好让从机读取被主机拉低的SDA

不发送应答信号:

/**********不发送应答信号********/
void I2C_NoAck(void)
{
	SCL_LOW;
	SDA_HIGH;//SDA线不拉低
	I2C_Delay(100);
	SCL_HIGH;
	I2C_Delay(100);
}

主机不发送应答信号时,只需要维持SDA线是高电平即可。

使用I2C发送一个字节的数据:

/***********发送一个字节数据**********/
void I2C_WriteByte(uint8_t data)
{
	uint8_t i = 0;
	//8个比特位,8个clk
	for(i = 0; i< 8; i++)
	{
		SCL_LOW;//SCL拉低
		I2C_Delay(100);
		
		if(data & 0x80)
		{
			//发送数据的高位是1,拉高SDA
			SDA_HIGH;
		}
		else
		{
			//发送数据的高位是0,拉低SDA
			SDA_LOW;
		}
		data <<= 1;//左移1位,方便下次判断次高位
		SCL_HIGH;//SCL拉高
		I2C_Delay(100);
	}
	I2C_GetAck();//8个clk结束后,获取应答信号
}

一个字节有8个比特位,所以需要8个clk来发送一个字节的数据,每发送一个比特位时,先将SCL拉低并维持,然后判断要发送数据data的高位。

如果高位是1,则将SDA拉高,如果是高位是0,则将SDA拉低,然后将数据data左移移位,方便下次判断次高位,并且将SCL线拉高保持,好让对方读取SDA状态。

当8个比特位全部发送完毕后,去获取接收方的应答信号。

  • 在发送一个字节数据的时候,先判断的是data的高位,并且通过SDA线发送,所以发送一个字节是按照从高位到低位的顺序发送的。

读取一个字节数据:

/**********读取一个字节数据***********/
uint8_t I2C_ReadByte(uint8_t ack)
{
	uint8_t i = 0;
	uint8_t data = 0;
	
	SDA_HIGH;//SDA拉高放弃控制权
	//8个比特位,读取8次
	for(i = 0; i < 8; i++)
	{
		SCL_LOW;//SCL拉低,让从机改变SDA状态
		I2C_Delay(100);
		SCL_HIGH;//SCL拉高
		I2C_Delay(100);
		
		data <<= 1;//高位左移移位,方便接收次高位
		if(SDA_IN == 1)
		{
			//SDA高电平
			data++;
		}
	}
	
	//决定要不要给从机应答
	if(ack == 0)
	{
		I2C_Ack();//接收完毕,给从机应答信号
	}
	else if(ack == 1)
	{
		I2C_NoAck();//接收完毕,不给从机应答信号
	}
	
	return data;//返回接收到的数据
}

从机向主机发送数据时,先将SDA拉高放弃SDA线的控制权,此时SDA线的状态由从机决定。一个字节8个比特位,所以需要8个clk读取8次,每次读取时,先将SCL拉低,此时从机才能改变SDA电平状态,才能发送数据,然后保持一段时间后再拉高SCL,此时主机读到的SDA数据才是准确的。

将存放数据的data左移一位,方便接收次高位,当SDA线的电平是高时,data加一,如此反复八次。

  • 这个过程中,先接收到的比特位是高位,所以会被不停左移,八次读取后得到的8个比特位拼成一个字节的数据。
  • 和发送时先发送高位相对应。

一个字节数据读取完毕后,根据ack形参的值决定要不要给从机应答信号,最后再将接收到的数据data返回。

😽OLED显示

在OLED屏上还有一块驱动芯片,它是用来让屏幕显示内容的,我们让OLED显示内容其实就是在控制这块驱动芯片,本喵使用的OLED是SSD1306驱动芯片。

🙈SSD1306

特点:

  • 128×64点阵面板,也就是一共有8192个点。
  • 有256阶对比度可调节。
  • 支持6800/8080并行总线。
  • 支持SPI、I2C串行总线。
  • 支持水平方向和垂直方向的滚动。
  • 支持行或列的重映射,也就是反转方向。

设备地址:

图
从芯片手册中可以看到,该芯片的地址有7位,从b1~b7,其中b2~b7是固定的,二进制序列是0111 10b1是由芯片的D/C引脚决定的。
图
从上面的芯片电路图中可以看到,D/C引脚是接地的,所以b1的值就是0,所以该芯片的地址就是0111 100,通过这7个比特位可以找到这个芯片。

b0是读写控制位,1表示从该芯片中读取数据,0表示向该芯片写入数据,所以:

  • 0b0111 1000十六进制0x78是写数据时的设备地址。
  • 0b0111 1001十六进制0x79是读数据时的设备地址。
/***************定义设备SSD1306读写地址*************/
#define OLED_WRITE_ADDR 		0x78		//写地址
#define OLED_READ_ADDR			0x79		//读地址

/***************定义设备控制命令**************/
#define OLED_WRITE_CMD			0x00		//向OLED写命令
#define OLED_WRITE_DATA			0x40		//向OLED写数据

在代码中使用宏来定义设备的读写地址,以及告诉从设备是写命令还是写数据。

🙈 SSD1306的I2C总线数据格式

图
如上图所示便是和SSD1306控制芯片通信的I2C总数据格式,主机STM32F103ZET6首先发送起始信号S,然后发送设备地址(由7位Slave Address和1位R/W组成一个字节),再读取从机SSD1306的应答信号。

得到应答信号以后再发送一个控制字节,告诉SSD1306芯片,接下来的数据是控制命令还是向驱动芯片的GRAM中写入数据。

图
如上图所示便是控制字节,其中Co位表示该字节中紧跟着的数据是仅有数据字节还是会包含控制字节,默认为0,D/C位为1表示紧跟着的字节数据为写入驱动芯片GRAM的数据,为0则表示这是一个命令数据。

写命令:

/***********写命令**********/
void OLED_WriteCmd(uint8_t cmd)
{
	I2C_Start();//开始信号
	I2C_WriteByte(OLED_WRITE_ADDR);//写从送设备地址
	I2C_WriteByte(OLED_WRITE_CMD);//告诉设备要写命令
	I2C_WriteByte(cmd);//写具体命令
	I2C_Stop();//停止信号
}

先产生开始信号,然后发送从设备SSD1306芯片地址(写函数中已经包含获取应答),再发送控制字节表明要向从设备中写命令,然后再写入具体的命令cmd,最后产生停止信号。

写一个字节数据:

/***********写一个字节数据**********/
void OLED_WriteDate(uint8_t data)
{
	I2C_Start();//开始信号
	I2C_WriteByte(OLED_WRITE_ADDR);//写从送设备地址
	I2C_WriteByte(OLED_WRITE_DATA);//告诉设备要写数据
	I2C_WriteByte(data);//写具体数据
	I2C_Stop();//停止信号
}

通信开始后,先写从设备地址,然后发送控制字节告诉从设备要写入数据,再写入具体的数据,最后产生停止信号。

写多个字节数据:

/***********写多个字节数据*********/
void OLED_WriteNBytes(uint8_t* buffer,	uint16_t length)
{
	uint16_t i = 0;
	if(buffer == NULL)	return;//源缓冲区为空直接返回
	I2C_Start();//开始信号
	I2C_WriteByte(OLED_WRITE_ADDR);//写从设备地址
	I2C_WriteByte(OLED_WRITE_DATA);//告诉从设备要写数据
	//写入多个字节
	for(i = 0; i< length; i++)
	{
		I2C_WriteByte(buffer[i]);
	}
	I2C_Stop();//停止信号
}

首先进行判断,如果写数据的源缓冲区为空,则直接返回,不为空则继续执行。通信开始后,同样需要写从设备地址并且告诉从设备要写数据,之后多次调用写一个字节的函数发送多个字节数据,最后停止通信。

🙈OLED的显示

图
如上图所示便是OLED的内部示意图,外部处理器STM32F103ZET6通过I2C协议将数据发送到OLED内部的MCU上,然后内部MUC将数据给到GDDRAM上存储,再将数据给到显示控制器,然后进行行/列地址驱动,最终在OLED屏幕上显示内容。

图
如上图所示是OLED屏幕示意图,整个拼命有128×64个像素点,分为128列,64行,由于一个字节有8个比特位,所以一列中每8行对应一个字节。

64个行又划分为8页,每一页有127列×8页个像素点。上图中,第一页PAGE0的第一列COL0对应的8个比特位是01010101,右边屏幕上对应比特位为1的像素点是白色,其他为黑色。

  • OLED的显示其实就是在填充这128×64个像素点。

当I2C发送多个字节数据的时候,显存GDDRAM又是如何保存这些数据的呢?保存这些数据有三种地址模式:页地址模式,垂直地址模式,水平地址模式。

本喵这里仅介绍最常用的页地址模式:

图
如上图所示,在页地址模式下,当往显存里面写入数据后,列地址指针会自动递增1,所以设置好起始页和起始列之后,就可以连续发送数据,而不用每发送一个数据就去指定一个页和列的地址了。

如果列地址指针递增到了设置的结束列地址,那么列地址指针就会复位回到设置的起始列地址,而页地址指针是不会有变化的。

  • 向下一页显存中存放数据时,用户必须设置新的页和列的起始地址。

图
如上图所示,这是从SSD1306芯片手册中截取的,用来设置显存的地址模式,主设备需要先向从设备发送0x20控制字节,表示要设置页地址模式,然后再发送一个字节范围为0x00~0x03的数据来指定地址模式。

设置地址模式:

typedef enum
{
    H_ADDR_MODE     = 0,    // 水平地址模式
    V_ADDR_MODE     = 1,    // 垂直地址模式
    PAGE_ADDR_MODE  = 2,    // 页地址模式
}MEM_MODE;  // 内存地址模式

static MEM_MODE mem_mode = PAGE_ADDR_MODE;
void OLED_SetMemAddrMode(MEM_MODE mode)
{
	if((mode != H_ADDR_MODE) && (mode != V_ADDR_MODE) && (mode != PAGE_ADDR_MODE))      return;
    OLED_WriteCmd(0x20);
    OLED_WriteCmd(mode);
    mem_mode = mode;
}

根据芯片手册所描述的,给从设备发对应的数据就可以设置成页地址模式,这也是一种最常用的地址模式。


图
如上图所示,这是用来设置页起始地址的,在写指令时发送一个字节范围是0xB0~0xB7的数据,其中低3位的值是告诉显存要将数据存放在哪一页。

设置起始页地址:

#define PAGE_ADDR_MODE_BASE			0xB0
void OLED_SetPageAddr_PAGE(uint8_t addr)
{
	if(mem_mode != PAGE_ADDR_MODE)  return;
	if(addr > 7)   return;
	OLED_WriteCmd(PAGE_ADDR_MODE_BASE + addr);
}

在调用该函数的时候,可以指定起始页地址,但是不能超过7,因为一共有8页,判断合法后,写命令写入起始页地址的值。

芯片手册中,D7~D3的值是固定的0b1011 0,所以PAGE_ADDR_MODE_BASE0xB0,页地址在这个基础上作偏移即可。


还有屏幕的打开和关闭等等:

#define DISP_ON()             	OLED_WriteCmd(0xAF) //开始显示
#define DISP_OFF()            	OLED_WriteCmd(0xAE)	//关闭显示

设置起始列地址等等功能的方法等等,大家可以自己对着芯片手册去查找它的使用规则,本喵后面会将源码及手册分享出来。

OLED初始化

题

如上图所示是OLED整个初始化过程,这个过程图在芯片手册中也有,我们只需要按照流程挨个调用自己实现的功能函数即可。

初始化:

void OLED_Init(void)
{   
    OLED_SetMemAddrMode(PAGE_ADDR_MODE);    			// 0. 设置地址模式
    OLED_SetMuxRatio(0x3F);                 			// 1. 设置多路复用率
    OLED_SetDispOffset(0x00);               			// 2. 设置显示的偏移值
    OLED_SetDispStartLine(0x00);            			// 3. 设置起始行
    OLED_SEG_REMAP();                       			// 4. 行翻转
    OLED_SCAN_REMAP();                      			// 5. 翻转扫描
    OLED_SetComConfig(COM_PIN_SEQ, COM_NOREMAP);  		// 6. COM 引脚设置
    OLED_SetContrastValue(0x7F);            			// 7. 设置对比度
    ENTIRE_DISPLAY_OFF();                   			// 8. 背景熄灭
    DISP_NORMAL();                          			// 9. 显示模式
    OLED_SetDCLK_Freq(0x00, 0x08);          			// 10. 设置分频系数和频率增值
    OLED_SetChargePump(PUMP_ENABLE);        			// 11. 使能电荷碰撞
    
    OLED_SetComConfig(COM_PIN_ALT, COM_NOREMAP);	//改变显示字体大小
    
    DISP_ON();																		//开始显示
}

其中第4步就是让原本在右边显示变成在左边显示,第5步是让原本在下面显示变成在上面显示,根据屏幕摆放的位置做好调整即可,本喵这里就是将原本从右下角开始显示变成从左上角开始显示。

显示字符

到目前在OLED上只能点亮指定位置的像素点,如果要显示字符还需要我们将字符对应的所有像素点点亮,通过字符生成工具,可以直接获得要显示的字符所有对应的数据。

图
如上图,设置成阴码显示,选择列行式以及逆向取模,然后输入字符A点击生成以后,就会生成一个长为16字节的数组,将这个数组中的数据发送给显存,就会显示出来字符A。

设置显示起始位置:

void OLED_SetPosition(uint8_t page, uint8_t col)
{
	OLED_SetPageAddr_PAGE(page);	//设置页起始地址
    OLED_SetColAddr_PAGE(col);		//设置列起始地址
}

设置显示起始位置,指定起始页地址和起始列地址。

	uint8_t ch[16] = {0x00,0x00,0xC0,0x38,0xE0,0x00,0x00,0x00,0x20,0x3C,0x23,0x02,0x02,0x27,0x38,0x20};/*"A",0*/
	I2C_GPIO_ReInit();	//I2C的GPIO配置
	OLED_Init();		//初始化OLED
	OLED_Clear();		//清屏

	OLED_SetPosition(0, 0);		//设置起始位置
	OLED_WriteNBytes(ch,16);	//发送16个字节

main.c中,初始化完成后,设置显示的起始地址是第0页的第0列,发送A字符对应的16个字节数据,显示字符A。

图

如上图所示,但是此时显示的并不是一个完整的字符A,这是因为,我们用软件生成的子模是8×16的,所以需要用两页来显示。

	OLED_SetPosition(0, 0);		//第0页
	OLED_WriteNBytes(ch,8);		//发送8字节
	
	OLED_SetPosition(1, 0);		//第1页
	OLED_WriteNBytes(ch + 8,8);//发送8字节

给第0页发送8字节数据,再给第1页发送8字节数据,此时字符A才能显示完整。
图

如上图,此时一个完整的字符就显示完成了,那么如果要显示字符串呢?难道把所有需要的字符都生成一遍吗?

图
同样使用该软件生成一个字库,该字库中包含所有ASCII码中的所有值。

图
如上图所示,该字库是一个二维数组,其中行号就对应着ASCII码值,所以根据行号就可以找到任何一个英文字母所对应的16字节数据,然后发给显存即可。

显示一个字符:

void OLED_PutChar(uint8_t page, uint8_t col, char c)
{
	OLED_SetPosition(page, col);										
	OLED_WriteNBytes((uint8_t*)&ascii_font[c][0],8);//根据ASCII码索引,发送前8字节
	
	OLED_SetPosition(page + 1, col);
	OLED_WriteNBytes((uint8_t*)&ascii_font[c][8],8);//根据ASCII码索引,发送后8字节
}

在发送一个字符的时候,同样需要两页来显示,根据ASCII码值,在二维数组中找到对应字符所对应的数据,第一页发送前8个数据,第二页发送后8个数据。

显示一个字符串:

void OLED_PrintString(uint8_t page, uint8_t col, char* str)
{
	while(*str != '\0')
	{
		OLED_PutChar(page,col,*str);
		col+=8;
		if(col > 127)
		{
			//127列显示满,调整页数
			page += 2;
		}		
		if(page > 7)
		{
			//全部显示满后,从头开始显示
			page = 0;
		}
		str++;
	}
}

调用该函数显示字符串的时候,传入一个字符串的形参,通过该指针将字符串中的所有字符挨个显示出来,直到遇到'\0'结束显示,当127列显示满后就需要调整页数,当页数满了以后,从头开始重新显示。

	EnableDebugIRQ();
	KEY_GPIO_ReInit();
	I2C_GPIO_ReInit();
	OLED_Init();
	OLED_Clear();

	OLED_PrintString(0,0,"I Love Shanghai");

main.c中执行上面代码就会显示I Love Shanghai字符串在OLED屏幕上。
图

如上图,成功显示字符串,它的大小样式等都可以调节,有兴趣的小伙伴可以自己研究一下。

😽源码及资料

本喵已经将源码,包括I2C驱动源,OLED驱动源码,还有字模制作工具,SSD1306驱动芯片等资源上传,有需要的小伙伴可以去下载。传送门

😽总结

用OLED屏幕来显示字符是人机交互的一种重要方式,也是本喵之后要做的小项目中的一部分,I2C在这个过程中扮演了非常重要的角色,通过应用OLED可以对I2C通信协议有一个清晰的认识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1031325.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

黄金代理如何选择平台?窍门在这儿

作为一个黄金代理平台&#xff0c;什么才是最重要的呢&#xff1f;笔者认为以下三个方面是最重要的&#xff0c;一个是资质&#xff0c;第二个是口碑&#xff0c;第三个是平台的软件。这三者是成为黄金代理要考虑的最重要的三个因素&#xff0c;也直接关系大黄金代理的职业生涯…

大模型股票交易-挖掘新闻和情绪价值

埃隆马斯克 (Elon Musk) 的星际飞船于 2023 年 4 月 20 日升空后爆炸。想象一下&#xff0c;当时您正在观察股市&#xff0c;突然出现新闻&#xff0c;您会如何交易 TSLA 股票&#xff1f; 我希望您不要与我争论&#xff0c;您作为交易者&#xff08;而不是投资者&#xff09;要…

基于Kubernetes的Serverless PaaS稳定性建设万字总结

作者&#xff1a;许成铭&#xff08;竞霄&#xff09; 数字经济的今天&#xff0c;云计算俨然已经作为基础设施融入到人们的日常生活中&#xff0c;稳定性作为云产品的基本要求&#xff0c;研发人员的技术底线&#xff0c;其不仅仅是文档里承诺的几个九的 SLA 数字&#xff0c…

【Redis】第6讲 Redis的启动和关闭

前端运行方式 进入/usr/local/bin目录下查看是否有redis-server rootsue-virtual-machine:/opt/redis-5.0.4# cd /usr/local/bin/ 然后启动redis-server服务器 rootsue-virtual-machine:/usr/local/bin# redis-server 后台运行方式 前端运行的话&#xff0c;界面中输入任何…

MTR 网络连通性测试工具 基础入门 整理

MTR MTR的全称是 my traceroute&#xff0c;是一个集合了 ping 与 traceroute 功能的网络诊断工具&#xff0c;广泛应用于链路测试。相对于 traceroute 只会做一次链路跟踪测试&#xff0c;mtr会对链路上的相关节点做持续探测并给出相应的统计信息。因此&#xff0c;mtr能避免…

计算机网络 实验二 交换机的基本配置

实验二 交换机的基本配置 实验目的 • 掌握交换机的配置方式及切换命令&#xff1b; • 掌握交换机端口的基本配置&#xff1b; • 掌握交换机mac地址的查看与管理方法。 实验设备 以太网交换机一台服务器一台PC机五台配置电缆、网线若干 网络拓扑及IP地址分配 给计算…

AI写作生成器,文章生成器

AI写作生成器&#xff0c;也叫文章生成器。你是否常常为了创作大量文案而感到疲惫不堪&#xff1f;是否曾经为了一篇好的博客、一个广告文案或一封邮件而煞费苦心&#xff1f;AI写作生成器可以帮助你轻松解决这些问题&#xff0c;让文案创作变得如丝般顺滑。 147GPT批量文章生成…

Premiere Pro切换中文

安装了Premiere Pro突然发现界面语言是英文版的&#xff0c;首选项里也没有UI语言切换选项&#xff0c;怎么在不重装的情况下&#xff0c;将英文版Premiere Pro怎么切换成中文&#xff1f; 1、打开安装好的Premiere Pro&#xff0c;可以看到界面语言是英文版的; 2、进入pr后,新…

【Java 基础篇】Java网络编程实战:P2P文件共享详解

Java网络编程是现代软件开发中不可或缺的一部分&#xff0c;因为它允许不同计算机之间的数据传输和通信。在本篇博客中&#xff0c;我们将深入探讨Java中的P2P文件共享&#xff0c;包括什么是P2P文件共享、如何实现它以及一些相关的重要概念。 什么是P2P文件共享&#xff1f; …

Socket编程基础(1)

目录 预备知识 socket通信的本质 认识TCP协议和UDP协议 网络字节序 socket编程流程 socket编程时常见的函数 服务端绑定 整数IP和字符串IP 客户端套接字的创建和绑定 预备知识 理解源IP和目的IP 源IP指的是发送数据包的主机的IP地址&#xff0c;目的IP指的是接收数据包…

猫头虎博主的AI魔法课:一起探索CSDN AI工具集的奥秘!

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

Merge之后,还没有Push,如何回滚

Merge之后&#xff0c;还没有Push&#xff0c;如何回滚 Merge之后&#xff0c;还没有Push&#xff0c;如何回滚 1&#xff1a;代码操作&#xff1a; 1&#xff1a;git log 查看git执行历史记录 GIT所有的执行记录会以倒叙呈现&#xff1b;最上面的就是需要回滚的merge序列号&a…

凹凸贴图和法线贴图的区别

1、什么是凹凸贴图 凹凸贴图&#xff08;bump mapping&#xff09;是一种计算机图形学中的渲染技术&#xff0c;用于在给定的表面上模拟微小的凹凸纹理。通过在表面法线方向上微调每个像素的光照值&#xff0c;可以给平滑的表面增加视觉上的凹凸感。 在凹凸贴图中&#xff0c;每…

第一百五十回 自定义组件综合实例:游戏摇杆一

文章目录 概念介绍实现方法示例代码我们在上一章回中介绍了自定义组件相关的内容,本章回中将综合使用这些内容 自定义游戏摇杆组件.闲话休提,让我们一起Talk Flutter吧。 概念介绍 我们介绍的游戏摇杆就是一个内层的小圆嵌套一个外层的大圆,大圆的位置不变,小圆只能在大圆…

读取jsonArray文件并转换为java对象工具类

json文件中存放jsonArray&#xff0c;将其读取出来并转换为java对象&#xff0c;转换的对象需要根据传入的对象动态转换&#xff0c;工具类编写如下&#xff1a; import lombok.extern.slf4j.Slf4j; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOEx…

接口自动化测试TestNG框架环境搭建

TestNG是什么&#xff1f; TestNG是一个功能强大的测试框架&#xff0c;是Junit的一个增强版本&#xff0c;Junit在使用多年之前&#xff0c;TestNG才生效存在。NG 代表“下一代”。 TestNG框架提供了以下功能和解答我们的问题&#xff1a;“为什么我们需要TestNG”&#xff…

39 | selenium基础架构,UI测试架构

什么是测试基础架构&#xff1f; 测试基础架构指的是&#xff0c;执行测试的过程中用到的所有基础硬件设施以及相关的软件设施。因此&#xff0c;我们也把测试基础架构称之为广义的测试执行环境。通常来讲&#xff0c;测试基础架构主要包括以下内容&#xff1a; 执行测试的机器…

BANI时代下,项目如何实现价值交付?

随着时代的变化&#xff0c;继VUCA时代后、新的语言出现&#xff1a;BANI一词逐渐流行起来。BANI&#xff0c;取自四个英文单词 Brittle&#xff08;脆弱的&#xff09;、Anxious&#xff08;焦虑的&#xff09;、Nonlionear&#xff08;非线性的&#xff09;、Incomprehensibl…

CodeTON Round 6 (Div. 1 + Div. 2, Rated, Prizes!)

A.MEXanized Array AC代码: #include<iostream> #include<algorithm> #include<cstring> using namespace std; const int N210; int a[N]; int n,k,x; void solve() {cin>>n>>k>>x;if(x<k-1) {cout<<-1<<endl;return;}i…