西安优秀的集团门户网站建设企业/女教师网课入06654侵录屏
🐱作者:一只大喵咪1201
🐱专栏:《STM32学习》
🔥格言:你只管努力,剩下的交给时间!
今天需要将代码烧录到开发板中,本喵默认大家都会创建工程,以及进行基本的外设配置。
I2C通信协议 | OLED屏
- 😽I2C协议
- 🙈数据格式
- 🙈I2C信号时序
- 🙈I2C驱动代码
- 😽OLED显示
- 🙈SSD1306
- 🙈 SSD1306的I2C总线数据格式
- 🙈OLED的显示
- OLED初始化
- 显示字符
- 😽源码及资料
- 😽总结
😽I2C协议
I2C协议是一种通信协议,通常用来在主设备和从设备之间进行通信,本喵使用的主设备是STM32F103ZET6
芯片的开发板,从设备使用的是SSD1306
芯片驱动的OLED
屏幕。
I2C在硬件上的接法如上图所示,主控芯片引出两条线SCL
和SDA
线,在一条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
的真值表如下:
A | B | SDA |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
- 通过真值表可以看到,
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个clkfor(i = 0; i< 8; i++){SCL_LOW;//SCL拉低I2C_Delay(100);if(data & 0x80){//发送数据的高位是1,拉高SDASDA_HIGH;}else{//发送数据的高位是0,拉低SDASDA_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 10
,b1
是由芯片的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_BASE
为0xB0
,页地址在这个基础上作偏移即可。
还有屏幕的打开和关闭等等:
#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(); //初始化OLEDOLED_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通信协议有一个清晰的认识。
相关文章:

【STM32学习】I2C通信协议 | OLED屏
🐱作者:一只大喵咪1201 🐱专栏:《STM32学习》 🔥格言:你只管努力,剩下的交给时间! 今天需要将代码烧录到开发板中,本喵默认大家都会创建工程,以及进行基本的…...

Nvme Spec 第一章节学习
Nvme Express Base Specification 第一章 简介 1.1概述 NVM ExpressTM(NVMeTM)接口允许主机软件与非易失性存储器子系统通信。 此接口针对企业和客户端固态驱动器进行了优化,通常作为寄存器级接口连接到PCI Express接口。 注:在…...

第一章:最新版零基础学习 PYTHON 教程(第九节 - Python 语句中的 – 多行语句)
Python 中的语句: 在Python中,语句是Python解释器可以读取和执行的逻辑命令。它可能是Python 中的赋值语句或表达式。 Python 中的多行语句: 在Python中,语句通常写成一行,每行的最后一个字符是换行符。要将语句扩展到一行或多行,我们可以使用大括号 {}、圆括号 ()、方…...

kafka 3.0 离线安装
1.安装zookeeper 解压apache-zookeeper-3.8.0-bin.tar.gz到指定目录,复制conf目录下zoo_sample.cfg到zoo.cfg,并修改配置。 # The number of milliseconds of each tick tickTime=2000 # The number of ticks that the initial # synchronization phase can take initLimit…...

MySQL数据库入门到精通2--基础篇(函数,约束,多表查询,事务)
3. 函数 函数 是指一段可以直接被另一段程序调用的程序或代码。MySQL中的函数主要分为以下四类: 字符串函数、数值函数、日期函数、流程函数。 3.1 字符串函数 MySQL中内置了很多字符串函数,常用的几个如下: 演示如下: A. con…...

c-数据在内存中的存储-day7
...

3D大模型如何轻量化?试试HOOPS Communicator,轻松读取10G超大模型!
随着计算机技术的不断发展,3D模型在各行各业中的应用越来越广泛。然而,随着模型的复杂性和规模不断增加,处理和浏览超大型3D模型变得越来越具有挑战性。本文将探讨如何轻量化3D大模型,以及如何使用HOOPS Communicator来读取和浏览…...

go并发操作且限制数量
使用管道chan func returnNum() int64 {return time.Now().Unix() } func main() {threadAmount : runtime.GOMAXPROCS(0)if threadAmount < 2 {threadAmount 2}fmt.Println(threadAmount)threadChan : make(chan int, threadAmount)defer close(threadChan)for {for i :…...

AI深度学习-卷积神经网络000
文章目录 前言1.什么是深度学习2.语义分割与实例分割概述3.什么是卷积?4.Unet网络 前言 本栏目,主要为深度学习保姆教程。 主要通过B站视频整理而来: 深度学习保姆级教学 Unet语义分割视觉三维重建算法 1.什么是深度学习 深度学习保姆级教…...

网站有反爬机制就爬不了数据?那是你不会【反】反爬
目录 前言 一、什么是代理IP 二、使用代理IP反反爬 1.获取代理IP 2.设置代理IP 3.验证代理IP 4.设置代理池 5.定时更新代理IP 三、反反爬案例 1.分析目标网站 2.爬取目标网站 四、总结 前言 爬虫技术的不断发展,使得许多网站都采取了反爬机制ÿ…...

2023华为杯研究生数学建模C题分析
完整的分析查看文末名片获取! 问题一 在每个评审阶段,作品通常都是随机分发的,每份作品需要多位评委独立评审。为了增加不同评审专家所给成绩之间的可比性,不同专家评审的作品集合之间应有一些交集。但有的交集大了,则…...

第三天:实现网络编程基于tcp/udp协议在Ubuntu与gec6818开发板之间双向通信
互联网地址 每一台设备接入互联网后,都会举报一个唯一的地址编号 IP地址 INTERNET地址 internet地址 :它是协议上的一个逻辑地址 目前来说,我们主要的IP地址有两类 IPV4 IPV6 IPV4 其实就是使用一个32bit整数作为IP IPV6 其实就是使用一…...

【MediaSoup---源码篇】(三)Transport
概述 RTC::Transport是mediasoup中的一个重要概念,它用于在mediasoup与客户端之间传输实时音视频数据。 Transport继承着众多的类,主要用于Transport的整体感知 class Transport : public RTC::Producer::Listener,public RTC::Consumer::Listener,publ…...

爱分析《商业智能最佳实践案例》
近日,国内知名数字化市场研究咨询机构爱分析发布《2023爱分析商业智能最佳实践案例》,此评选活动面向落地商业智能的各行企业和商业智能厂商,以第三方专业视角深入调研,评选出具有参考价值的创新案例。永达汽车集团与数聚股份合作…...

golang:context
context作用 goroutine的退出机制 多个goroutine都是平行的被调度的,多个goroutine如何协调工作涉及通信、同步、通知和退出 通信:goroutine之间的通信同步chan通道 同步:不带缓冲的chan提供了一个天然的同步等待机制。通过WaitGroup也可以…...

探讨代理IP与Socks5代理在跨界电商中的网络安全应用
在数字化时代,跨界电商已经成为了商业世界中的一大趋势。然而,跨越国界的电商活动也伴随着网络安全挑战。本文将讨论如何利用代理IP和Socks5代理技术来提高跨界电商中的网络安全,同时也探讨了与游戏相关的爬虫应用。 1. 代理IP和Socks5代理的…...

Guava Cache介绍-面试用
一、Guava Cache简介 1、简介 Guava Cache是本地缓存,数据读写都在一个进程内,相对于分布式缓存redis,不需要网络传输的过程,访问速度很快,同时也受到 JVM 内存的制约,无法在数据量较多的场景下使用。 基…...

ARM 汇编指令作业(求公约数、for循环实现1-100之间和、从SVC模式切换到user模式简单写法)
1、求两个数最大公约数 .text .globl _start_start:mov r0, #9mov r1, #15 Loop: 循环cmp r0,r1 比较r0和r1的大小beq stop 当r0和r1相等时,跳到stop标签subhi r0,r0,r1 r0-r1>0 时,证明r0>r1,将r0-r1的值赋给r0&…...

Go - 【字符串,数组,哈希表】常用操作
一. 字符串 字符串长度: s : "hello" l : len(s) fmt.Println(l) // 输出 5遍历字符串: s : "hello" for i, c : range s {fmt.Printf("%d:%c ", i, c) } // 输出:0:h 1:e 2:l 3:l 4:ofor i : 0; i < le…...

vue 普通组件的 局部注册
vue 普通组件的 注册 11 Vue2_3入门到实战-配套资料\01-随堂代码素材\day03\素材\00-准备代码\小兔鲜首页静态页\src...

医疗虚拟仿真和虚拟现实有什么区别?哪个更好?
随着我们在仿真教育中越来越多地使用新技术,区分虚拟模式的类型很重要。虚拟仿真是一个统称,用来概括术语来描述各种基于仿真的体验,从基于屏幕的平台到沉浸式虚拟现实。然而,各虚拟平台在保真度、沉浸感和临场感的水平上有很大差…...

【.net core】yisha框架使用nginx代理swagger接口无法访问问题
后端代码配置 #在StartUp.cs文件中Configure方法中增加以下代码 app.UseSwagger(c >{//代理路径访问c.PreSerializeFilters.Add((doc, item) >{//根据代理服务器提供的协议、地址和路由,生成api文档服务地址doc.Servers new List<OpenApiServer>{ new…...

uniapp录音功能和音频播放功能制作
录音功能 在 UniApp 中,你可以使用 uni.getRecorderManager() 方法来创建一个录音管理器实例,从而实现录音功能。 以下是一个示例,演示了如何在 UniApp 中使用 uni.getRecorderManager() 实现录音功能: // 在需要录音的页面或组…...

服务器数据恢复-LINUX操作系统下各文件系统误删除/格式化数据的恢复方案
服务器数据恢复环境: 基于EXT2/EXT3/EXT4/Reiserfs/Xfs文件系统的Linux操作系统。 服务器故障: LINUX操作系统下误删除/格式化数据。 服务器数据恢复过程: 1、首先会检测服务器是否存在硬件故障,如果检测出硬件故障,交…...

python/C++二分查找库函数(lower_bound() 、upper_bound,bisect_left,bisect_right)
二分查找是一种经典的搜索算法,广泛应用于有序数据集中。它允许在大型数据集中高效地查找目标元素,减少了搜索的时间复杂度。本文将介绍在 C 和 Python 中内置的二分查找函数,让二分查找变得更加容易。 c lower_bound() 、upper_bound 定义…...

爬虫 — App 爬虫(二)
目录 一、Appium介绍二、node.js 安装三、Java 的 SDK 安装以及配置1、安装步骤2、配置环境变量 四、安卓环境的配置1、配置环境变量 五、Appium 安装1、安装2、打开 APP3、使用 六、Appium 使用1、定位数据(方法一,不常用)2、定位数据&#…...

汽车电子相关术语
SOA SOA(Service-Oriented Architecture,面向服务的架构)是一种在计算机环境中设计、开发、部署和管理离散模型的方法。是由Garnter1996年提出的概念,将应用程序的不同功能单元(称为服务)进行拆分…...

Python 找出最大数
"""在输入的三个数中找出最大知识点:1、条件嵌套语句if/else2.字符串分割函数split()3、列表元素索引4、数据类型转换举一反三:1、如何控制只能输入三个数,否则重新输入2、如何避免输入无效字母"""# 定义一个变…...

Spring Security 用了那么久,你对它有整体把控吗?
文章目录 1.Servlet Filter:守门人的角色2.DelegatingFilterProxy:桥接 Servlet 和 Spring 的神器3.FilterChainProxy:Spring Security 过滤器链的管家3.SecurityFilterChain:Security 过滤器的串绳4.Spring Security 中的过滤器机…...

vue+minio实现文件上传操作
vueminio实现文件上传操作 minio文件上传vueminio实现文件上传操作 minio文件上传 minio文件上传有两种方法: 第一种是通过ak,sk,调用minio的sdk putObject进行文件上传;该方法支持go,java,js等各种语言&…...