【stm32入门学习I2C_铁头山羊】

1.I2C基本电路结构

在这里插入图片描述
I2C 通信中,SDA(数据线)和 SCL(时钟线)都是采用开漏(open-drain/漏极开路)模式来驱动的,主要原因如下:

避免总线冲突:I2C 总线是多主多从结构,任何一个设备都可以拉低(驱动低电平)总线,但没有设备能主动拉高总线(只能通过上拉电阻实现高电平)。如果一个设备输出高电平、另一个设备输出低电平,就可能导致短路冲突。开漏输出可以避免这种冲突,因为开漏模式下只有拉低信号,不会主动拉高信号。

信号同步:由于 I2C 总线的开漏特性,多个设备可以同时连接在 SDA 和 SCL 上,各设备只需在需要时拉低信号,而上拉电阻会自动将信号恢复为高电平。这种设计使得多个设备可以同步工作,同时保证总线的安全性和稳定性。

复用功能:在很多微控制器(如 STM32)中,I2C 的 SDA 和 SCL 引脚通常通过设置为“开漏复用输出”来实现多功能配置,即既可以作为普通 GPIO 使用,也可以通过复用功能来作为 I2C 通信端口。这使得一个引脚可以兼顾多种功能,增加了芯片的灵活性。

2.I2C通信协议

  1. I2C通信的基本流程
  2. I2C的数据帧格式
  3. 起始位和停止位
  4. 寻址
  5. 传输数据
  6. 示例

1. I2C通信的基本流程

主机主动发送一个起始位,然后向从机发送7位地址+读(1)写(0)位(共8位)。然后发送/接收数据,数据传输完成后,发送停止位。
在这里插入图片描述
2. I2C的数据帧格式

在这里插入图片描述
与串口一次只能传输8-9个bit相比,I2C一次可以传输很多个字节。
3. 起始位和停止位
在这里插入图片描述
起始位:时钟线保持高电压,SDA下降沿。
下降沿:时钟线保持低电压,SDA上升沿。
4. 寻址

在这里插入图片描述
主机发送地址,等待从机发送ACK后表示寻址成功。
5. 传输数据
在这里插入图片描述
5.示例在这里插入图片描述

3.I2C模块的使用方法

1.I2C模块简介
在这里插入图片描述
stm32的一个片上外设,给单片机提供一个I2C接口。

2.IO引脚初始化
在这里插入图片描述
在这里插入图片描述

查stm103芯片数据手册知道I2C1的复用引脚位PB6和PB7,重映射引脚位为PB8和PB9。I2C2只有复用引脚位,没有重映射。
3.代码编写
GPIO 和 AFIO 连接在 APB2 总线上,因此使用 RCC_APB2PeriphClockCmd 来控制时钟。

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
	
	GPIO_PinRemapConfig(GPIO_Remap_I2C1, ENABLE);
	//对pb8和pb9初始化
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
	

4.连接电路图
我没有,暂且跳过。
5. 在这里插入图片描述
`void My_I2C_Init(void)
{
// 1. IO初始化
// 对I2C1进行重映射
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

GPIO_PinRemapConfig(GPIO_Remap_I2C1, ENABLE);
//对pb8和pb9初始化
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;

GPIO_Init(GPIOB, &GPIO_InitStruct);

RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, ENABLE);
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, DISABLE);

I2C_InitTypeDef I2C_InitStruct;

I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
I2C_InitStruct.I2C_ClockSpeed = 400000;
I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_Init(I2C1, &I2C_InitStruct);

I2C_Cmd(I2C1, ENABLE);

}

4.I2C写数据

I2C模块内部框图

在这里插入图片描述

数据发送过程简介:

1.起始位:向黄色的start写入1后,SDA发送一个下降沿。
2. 7位地址:将七位地址写入发送数据寄存器后,会自动的由SDA发送出去,然后等待ACK.
3. 数据发送:将数据写如发送寄存器后,等待状态寄存器的标志位改变,改变后,继续写入数据。
4. 停止位:给STOP写入1。

具体实现

在这里插入图片描述

  1. 等待总线空闲—判断是否位SR2寄存器的BUSY
  2. 发送起始位—SB为0发送未完成,1 为发送完成。SB位硬件置0。
  3. 发送地址。—清除AF, 发送地址。判断发送成功后跳出。ADDR==1。
  4. 清除地址ADDR。读SR1,读SR2。
  5. 发送数据— 判断上一个数据是否发送完成,且没有收到AF=1.
  6. 发送停止位----移位寄存器为空,发送数据寄存器为空—I2C_FLAG_BTF==1,发送停止位。
int My_I2C_SendBytes(I2C_TypeDef* I2Cx, uint8_t Addr, uint8_t *pData, uint16_t Size)
{
	//1.等待总线空闲
	while(I2C_GetFlagStatus(I2Cx, I2C_FLAG_BUSY) == SET);
	
	//2.发送起始位
	I2C_GenerateSTART(I2Cx, ENABLE);
	while(I2C_GetFlagStatus(I2Cx, I2C_FLAG_SB) == RESET);
	
	// 3. 发送地址
	//清除AF
	I2C_ClearFlag(I2Cx, I2C_FLAG_AF);
	//发送地址 + RW#
	I2C_SendData(I2Cx, Addr & 0xfe);
	while(1)
	{
		if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_ADDR) == SET) break;
		if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_AF) == SET)
		{
			I2C_GenerateSTOP(I2Cx, ENABLE);
			return -1;
		}
	}
	// 4.清除ADDR
	I2C_ReadRegister(I2Cx, I2C_Register_SR1);
	I2C_ReadRegister(I2Cx, I2C_Register_SR2);
	//5.发送数据
	for(uint16_t i = 0; i < Size; i++)
	{
		while(1)
		{
			if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_AF) == SET)
			{
				I2C_GenerateSTOP(I2Cx, ENABLE);
				return -2;
			}
			if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_TXE) == SET)
			{
				break;
			}
		}
		I2C_SendData(I2Cx, pData[i]);
	}
	
	while(1)
	{
		if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_AF)== SET)
		{
			I2C_GenerateSTOP(I2Cx, ENABLE);
			return -2;
		}
		if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_BTF) == SET)
		{
			break;
		}
	}
	// 6. 发送停止位
	I2C_GenerateSTOP(I2Cx, ENABLE);
	return 0;
}

5.I2C读数据

在这里插入图片描述
在这里插入图片描述
BUSY代表总线是否忙,TxE代表发送使能(表示发送数据寄存器为空,可以填入新的数据。),SB代表START位发送完成,ADDR表示寻址成功(需要软件清零),AF表示ACK Failure(1为失败,0不能说成功,只能说目前没有失败),需要软件清零。

1.读取数据的流程简介

主机发送起始位, 然后发送七位地址+rw, 然后后等待从机应答ack。ack收到后进行数据接收阶段,否则直接发送一个停止位。数据接收阶段:从机发送,主机接收,最后一个数据接受后主机一定要发送一个NAK,告知从机数据已经接收完毕,这点与发送数据的流程有所不同。

2. 发送起始位

等待总线空闲,然后给start写入1, 等待SR寄存器中的SB为1, 表示起始位发送完成。

3. 寻址阶段

首先把AF清零,然后给发送数据寄存器写入地址,然后等待从机ACK, 寻址成功的话ADDR为1, 失败的话AF为1。

//2.发送从机地址
	I2C_ClearFlag(I2Cx, I2C_FLAG_AF);
	I2C_SendData(I2Cx, Addr | 0x01);
	// 等待从机接收
	while(1)
	{
		if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_AF) == SET)
		{
			I2C_GenerateSTOP(I2Cx, ENABLE);
			return -1;
		}
		if(I2C_GetFlagStatus(I2Cx, I2C_FLAG_ADDR) == SET)
		{
			break;
		}
	}
	// 清除ADDR标志位
	I2C_ReadRegister(I2Cx, I2C_Register_SR1);
	I2C_ReadRegister(I2Cx, I2C_Register_SR2);

4.接收数据

Size = 1:
在这里插入图片描述
不能等数据接受完成后写入ack=0, ack只作用于当前接收的字节。
Size>2:
在这里插入图片描述

//3. 接收数据  
	if(Size == 1)
	{
		//向ACK写0
		I2C_AcknowledgeConfig(I2Cx, DISABLE);
		//写停止位1
		I2C_GenerateSTOP(I2Cx, ENABLE);
		//等待RxNE置位
		while(I2C_GetFlagStatus(I2Cx, I2C_FLAG_RXNE) == RESET);
		//读取数据
		pBuffer[0] = I2C_ReceiveData(I2Cx);
	}else
	{
		//ACK使能
		I2C_AcknowledgeConfig(I2Cx, ENABLE);
		for(uint16_t i = 0; i < Size; i++)
		{
			if(i == Size - 1)
			{
				I2C_AcknowledgeConfig(I2Cx, DISABLE);
				I2C_GenerateSTOP(I2Cx, ENABLE);
				while(I2C_GetFlagStatus(I2Cx, I2C_FLAG_RXNE) == RESET);
				pBuffer[i] = I2C_ReceiveData(I2Cx);	
			}else
			{
				//等待RXNE使能
				while(I2C_GetFlagStatus(I2Cx, I2C_FLAG_RXNE) == RESET);
				//取数据
				pBuffer[i] = I2C_ReceiveData(I2Cx);	
			}
		}
	}

6.软I2C

根据I2C数据帧的格式,在普通的IO引脚模拟波形实现I2C数据传输。
将GPIO的模式配置位开漏输出,STM32 的开漏输出模式是数字电路输出的一种,从结果上看它只能输出低电平 Vss 或者高阻态,常用于 IIC 通讯。
工作模式:
1.P-MOS 被“输出控制”控制在截止状态,因此 IO 的状态取决于 N-MOS 的导通状况;
2.施密特触发器是工作的,上拉和下拉均断开,可以看作浮空输入。
编写接口函数:

void scl_write(uint8_t level)
{
	if(level == 0)
	{
		GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);
	}else
	{
		GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_SET);
	}
}
void sda_write(uint8_t level)
{
	if(level == 0)
	{
		GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_RESET);
	}else
	{
		GPIO_WriteBit(GPIOA, GPIO_Pin_1, Bit_SET);
	}
}

uint8_t sda_read(void)
{
	if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) == Bit_SET)
	{
		return 1;
	}else
	{
		return 0;
	}
}
void delay_us(uint32_t us)
{
	uint32_t n = us * 8;
	for(uint32_t i = 0; i < n; i++);
}

1.发送起始位与停止位

在这里插入图片描述
起始位:因为有上拉电阻,所以总线默认位高电压。只用给SDA写入低电压就好。
停止位:SCL 保持高电平的状态下,SDA 从低电平跳变到高电平,便会生成一个停止位。上图不明显,看下面的图。
在这里插入图片描述
根据i2c的要求,SDA每一个变换,都要消耗一次SCL跳变。不能再SCL在高电压的时候给SDA拉低,那就等于起始位。所以应该SCL低电压的时候写入。

2.发送数据位

在这里插入图片描述

uint8_t SendByte(uint8_t Byte)
{
	for(uint8_t i = 0; i < 7; i++)
	{
		uint8_t x = Byte & 0x80;
		Byte = Byte << 1;
		scl_write(0);  
		sda_write(x);
		delay_us(1);
		scl_write(1);
		delay_us(1);
	}
	//读取ACK或NAK
	scl_write(0);
	sda_write(1);
	delay_us(1);
	scl_write(1);
	delay_us(1);
	return sda_read();
} 

3. 接收数据

在这里插入图片描述

uint8_t ReceiveByte(uint8_t ACK)
{
	uint8_t Byte = 0;
	for(uint8_t i = 0; i < 7; i++)
	{
		scl_write(0);
		sda_write(1);
		delay_us(1);
		scl_write(1);
		delay_us(1);
		Byte = (Byte | sda_read()) << 1;
	}
	scl_write(0);
	sda_write(!ACK);
	delay_us(1);
	scl_write(1);
	delay_us(1);
	return Byte;
}

7.将常用功能进行封装

int My_SI2C_SendBytes(SI2C_TypeDef *SI2C, uint8_t Addr, const uint8_t *pData, uint16_t Size)
{
	sda_write(1);
	scl_write(1);
	// #1. 发送起始位
	SendStart();
	// #2. 发送从机地址+RW
	if(SendByte(Addr & 0xfe) != 0)
	{
		SendStop();
		return -1; // 寻址失败
	}
	// #3. 发送数据
	for(uint16_t i=0; i<Size; i++)
	{
		if(SendByte(pData[i]) != 0)
		{
			SendStop();
			return -2; // 数据被拒收
		}
	}
	// #4. 发送停止位
	SendStop();
	return 0;
}
int My_SI2C_ReceiveBytes(SI2C_TypeDef *SI2C, uint8_t Addr, uint8_t *pBuffer, uint16_t Size)
{
	sda_write(1);
	scl_write(1);
	// #1. 发送起始位
	SendStart();
	// #2. 发送从机地址+RW
	if(SendByte(Addr | 0x01) != 0)
	{
		SendStop();
		return -1; // 寻址失败
	}
	// #3. 接收
	for(uint16_t i=0; i<Size; i++)
	{
		pBuffer[i] = ReceiveByte((i==Size-1) ? 1 : 0);
	}
	// #4. 发送停止位
	SendStop();
	return 0;
}

7.OLED的使用方法(软IIC)

在这里插入图片描述
在这里插入图片描述

1.屏幕初始化

通过OELD_Init函数实现初始化:

int OLED_Init(OLED_TypeDef *OLED, OLED_InitTypeDef *OLED_InitStruct)

初始化只需要初始化结构体,这个初始化结构体只有一个参数:

typedef struct
{
	int (*i2c_write_cb)(uint8_t addr, const uint8_t *pdata, uint16_t size); // i2c写数据回调函数
}OLED_InitTypeDef;

写回调函数。是i2c写数据的函数。
实现一下:

int i2c_write_bytes(uint8_t addr, const uint8_t *pData, uint16_t size)
{
	return My_SI2C_SendBytes(&si2c, addr, pData, size);
}

2. 基本概念与操作

1. 画笔,画刷

在这里插入图片描述

// @颜色
//
#define OLED_COLOR_TRANSPARENT 0x00 // 透明
#define OLED_COLOR_WHITE       0x01 // 白色
#define OLED_COLOR_BLACK       0x02 // 黑色

//
// @画笔
//
#define PEN_COLOR_TRANSPARENT OLED_COLOR_TRANSPARENT // 透明画笔
#define PEN_COLOR_WHITE       OLED_COLOR_WHITE // 白色画笔
#define PEN_COLOR_BLACK       OLED_COLOR_BLACK // 黑色画笔

//
// @画刷
//
#define BRUSH_TRANSPARENT OLED_COLOR_TRANSPARENT // 透明画刷
#define BRUSH_WHITE       OLED_COLOR_WHITE       // 白色画刷
#define BRUSH_BLACK       OLED_COLOR_BLACK       // 黑色画刷

1.设置画笔

void OLED_SetPen(OLED_TypeDef *OLED, uint8_t Pen_Color, uint8_t Width);

2.设置画刷

void OLED_SetBrush(OLED_TypeDef *OLED, uint8_t Brush_Color);

2. 屏幕

在这里插入图片描述

3. 光标

// @设置光标位置
void OLED_SetCursor(OLED_TypeDef *OLED, int16_t X, int16_t Y);
// @设置光标的X坐标
void OLED_SetCursorX(OLED_TypeDef *OLED, int16_t X);
// @设置光标的Y坐标
void OLED_SetCursorY(OLED_TypeDef *OLED, int16_t Y);
// @移动光标
void OLED_MoveCursor(OLED_TypeDef *OLED, int16_t dX, int16_t dY);
// @沿X轴方向移动光标
void OLED_MoveCursorX(OLED_TypeDef *OLED, int16_t dX);
// @沿Y轴方向移动光标
void OLED_MoveCursorY(OLED_TypeDef *OLED, int16_t dY);
// @获取光标当前位置
void OLED_GetCursor(OLED_TypeDef *OLED, int16_t *pXOut, int16_t *pYOut);
// @获取光标X坐标
int16_t OLED_GetCursorX(OLED_TypeDef *OLED);
// @获取光标Y坐标
int16_t OLED_GetCursorY(OLED_TypeDef *OLED);

4. 文字相关的操作

1. 打印字符串

在这里插入图片描述

2. 设置字体

1.找到自己的window字体库。
2.将字体文件放入到font文件夹中的ttfttc.
3. .map文件中写入要转换的文本内容,使用ttfttc转换字体的ttf文件到c语言头文件。
4.引用字体头文件,再调用OLED_SetFont接口:在这里插入图片描述

	//2.打印你好世界
	OLED_SetFont(&oled, &ht);
	uint16_t x = (OLED_GetScreenWidth(&oled) - OLED_GetStrWidth(&oled, str)) / 2;
	OLED_SetCursor(&oled, x, 28);
	OLED_DrawString(&oled, str);
	OLED_SendBuffer(&oled);

3.格式化打印字符串

OLED_Printf(&oled, "%04/%02/%02", 2024, 11, 6);

4. 设置文本区域

在这里插入图片描述

5. 绘图

1. 画点

在这里插入图片描述

2. 画线

在这里插入图片描述

3. 画矩形

在这里插入图片描述

4.绘制位图

在这里插入图片描述
如何准备位图:点击这里
在这里插入图片描述
选择字节

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值