STM32笔记归纳4:I2C

I2C

目录

I2C

一、基本电路结构

1.1.串口的缺点

1.2.I2C总线的电路结构

1.3.数据线和时钟线

1.4.逻辑线与

1.4.1.逻辑与

1.4.2.逻辑线与

1.5.主机如何发送时钟信号

1.6.主机如何发送数据

1.7.从机如何发送数据

1.8.为何上拉?为何开漏?

二、通信协议

2.1.I2C通信的基本流程

​编辑

2.2.I2C的数据帧格式

2.3.起始位和停止位

2.3.1.起始位

2.3.2.停止位

2.4.寻址

2.5.传输数据

2.6.例子

2.6.1.向从机0x78写两个字节(0x5a,0x33)

2.6.2.从从机0x20读一个字节(假设读到的是0x64)

三、I2C模块的使用方法

3.1.I2C模块简介

3.2.IO引脚的初始化

3.2.1.引脚分布

3.2.2.编码部分

3.3.连接电路

3.4.I2C的速度模式

3.5.时钟信号的占空比

3.6.I2C模块的初始化

3.6.1.编程接口

3.6.2.编码部分

四、写数据

4.1.I2C模块的内部结构框图

4.2.数据发送过程简介

4.2.1.发送字节函数

4.3.等待总线空闲

4.4.发送起始位

4.5.发送地址

4.6.发送数据

4.7.发送停止位

4.8.启动OLED

4.9.总代码

五、读数据

5.1.接收字节函数

5.2.数据读取的流程

5.3.发送起始位和地址

5.3.1.发送起始位

5.3.2.发送地址+读写位

5.4.如何发送ACK和NAK

5.5.如何发送停止位

[5.6.Size = 1](#5.6.Size = 1)

[5.7.Size = 2](#5.7.Size = 2)

[5.8.Size > 2](#5.8.Size > 2)

5.9.读取OLED

六、软I2C

[6.1.硬I2C VS 软I2C](#6.1.硬I2C VS 软I2C)

6.2.IO引脚初始化

6.3.IO读写和延迟函数

6.3.1.编程接口1

6.3.2.编程接口2

6.3.3.编程接口3

6.3.4.编程接口4

6.4.发送起始位和停止位

6.5.发送一个字节

6.6.接收一个字节

6.7.综合

6.8.测试

七、封装常用功能

7.1.测试硬件I2C

7.1.1.初始化硬件I2C代码

7.1.2.硬件I2C收发数据函数

7.1.3.发送数据

7.1.4.接收数据

7.2.测试软件I2C

7.2.1.软件I2C收发数据函数

7.2.2.收发数据代码

八、OLED显示器

8.1.OLED显示器的基本原理

8.2.屏幕初始化

8.2.1.初始化软I2C

8.2.2.屏幕初始化代码的编写

8.3.基本概念和操作

8.3.1.画笔和画刷

8.3.2.屏幕坐标系

8.3.3.光标

8.4.文字相关的操作

8.4.1.打印字符串

8.4.2.设置字体

8.4.3.格式化打印字符串

8.4.4.设置文本区域

8.5.绘图相关的操作

8.5.1.画点

8.5.2.画线

8.5.3.画矩形和画圆

8.5.4.绘制位图


一、基本电路结构

1.1.串口的缺点

一个串口只能连接一个串口设备

只能实现一对一的通信

而IIC通过一个通信接口就可以连接很多外设

1.2.I2C总线的电路结构

由时钟线和数据线组成,一般由单片机作为IIC总线主机

其他被连接设备为从机,这些设备都有SDA和SCL引脚

所有设备的SDA引脚连接在数据线上

所有设备的SCL引脚连接在时钟线上

时钟线与数据线都需接上拉电阻,并且都为开漏输出(逻辑线与)

每一个从机都有一个7位的从机地址

主机先发送从机地址与从机建立通信,然后与从机进行数据交互

1.3.数据线和时钟线

SDA:数据线,用来传输数据

数据信号:

**SCL:**时钟线,用来传输时钟

时钟信号:

注:

时钟信号总是由主机发送给从机(单向)

数据信号可以由主机发送给从机,也可以由从机发送给主机(双向)

1.4.逻辑线与

1.4.1.逻辑与

与运算:&

Y = X1 & X2 & X3 ... &Xn

只要 X1~Xn里任意一个等于0,结果为0

当X1~Xn全部都等于1,结果为1

1.4.2.逻辑线与

将所有设备的SCL引脚设置为输出开漏模式

向所有设备的SCL引脚写1,内部的N-MOS管断开

此时SCL引脚处于高阻抗,相当于引脚从电路断开

如果不接上拉电阻,时钟线就会处于悬空状态,导致电平不稳定

上拉电阻可以将悬空的时钟线拉到稳定的高电平,表示1

将所有设备的SCL引脚设置为输出开漏模式

向某个设备的SCL引脚写0,其余SCL引脚都写1

写1的引脚为高阻抗,写0的引脚输出低电压(相当于接地)

时钟线通过写0的SCL引脚接地输出低电平,表示0

1.5.主机如何发送时钟信号

发送低电平:

Y = 0 & 1 & 1 ... & 1 = 0(低电平)

发送高电平:

Y = 1 & 1 & 1 ... & 1 = 1 (高电平)

1.6.主机如何发送数据

发送0:

Y = 0 & 1 & 1 ... & 1 = 0(低电平)

发送1:

Y = 1 & 1 & 1 ... & 1 = 1 (高电平)

1.7.从机如何发送数据

发送0:

Y = 1 & 0 & 1 ... & 1 = 0(低电平)

发送1:

Y = 1 & 1 & 1 ... & 1 = 1 (高电平)

1.8.为何上拉?为何开漏?

向设备引脚写1,引脚处于高阻态,相当于开路,总线能够与上拉电阻相连,产生高电平

向设备引脚写0,引脚处于低电压,相当于接地,总线能够通过引脚端口电路接地,产生低电平

如果是推挽模式,一个设备写0为低电平,一个设备写1为高电平,直接造成短路

如果是下拉电阻,只能将总线拉到低电平,无法为总线提供高电平

二、通信协议

2.1.I2C通信的基本流程

  • 主机向总线发送发送起始位
  • 主机向总线发送7位从机地址+1位读写位
  • 主机和从机进行数据传输
  • 主机向总线发送停止位

2.2.I2C的数据帧格式

**注:**串口的数据帧每次只能传输8~9bit,I2C的数据帧每次可以传输多个字节

2.3.起始位和停止位

2.3.1.起始位
  • SCL = H
  • SDA ↓
2.3.2.停止位
  • SCL = H
  • SDA ↑

2.4.寻址

应答(Ack):

向主机SDA引脚写1,引脚处于高阻态,相当于开路,总线能够与上拉电阻相连,产生高电平

但在这过程中,若向从机SDA引脚写0,引脚处于低电压,相当于接地,总线能够通过引脚端口电

路接地,产生低电平,此时主机检测到低电压,接收到从机的应答信号

Y = 1 & 0 & 1 ... & 1 = 0(低电平)

注:

  • 串口是低位先行(LSB First),发送字节时从最低位开始
  • I2C是高位先行(MSB First),发送字节时从最高位开始
  • 0写1读

2.5.传输数据

**注:**发送方释放SDA,接收方拉低SDA表示应答

2.6.例子

2.6.1.向从机0x78写两个字节(0x5a,0x33)

寻址0x78(写):

0x78二进制:0111 1000

左移1位: 1111 0000

低位或0为写: 1111 000(7位地址)0(读写位)

写数据0x5a:

0x5a二进制:0101 1010

写数据0x33:

0x33二进制:0011 0011

2.6.2.从从机0x20读一个字节(假设读到的是0x64)

寻址0x20(读):

0x20二进制:0010 0000

左移1位: 0100 0000

低位或1为读: 0100 000(7位地址)1(读写位)

读数据0x64:

0x64二进制:0101 0100

I2C停止条件解读:

主机发送Nack:

  • **时机:**读取最后一个数据字节后,进入第九个SCL时钟周期
  • **电平:**主机作为接收方,保持SDA高电平,不进行拉低操作
  • **作用:**向从机表明这时最后一个字节,无需再发送数据

准备停止条件:

  • **时机:**Nack周期结束后,SCL线被主机拉低后
  • **电平:**在SCL为低电平时,主机将SDA从高电平拉到低电平
  • **作用:**为后续生成停止条件作准备

生成停止条件:

  • **时机:**主机拉高SCL线,使其处于高电平状态
  • **电平:**在SCL为高电平时,主机将SDA从低电平拉到高电平
  • **作用:**标志着通信结束,总线都被释放,处于空闲状态

三、I2C模块的使用方法

3.1.I2C模块简介

I2C模块是单片机上的片上外设,给单片机提供I2C通信接口

3.2.IO引脚的初始化

3.2.1.引脚分布

**复用:**CPU借助I2C模块来控制IO引脚

**开漏:**实现逻辑线与

3.2.2.编码部分
cpp 复制代码
//#1:初始化GPIO
	
/*开启AFIO模块的时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);	
/*使能I2C1的重映射*/
GPIO_PinRemapConfig(GPIO_Remap_I2C1,ENABLE);
/*开启GPIOB模块的时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
/*GPIO结构的前置声明*/
GPIO_InitTypeDef GPIO_InitStruct;
/*选择PB8和PB9引脚*/
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
/*设置复用输出开漏模式*/
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
/*最大输出速度尾2MHz*/
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
/*初始化PB8和PB9引脚*/
GPIO_Init(GPIOB,&GPIO_InitStruct);

3.3.连接电路

3.4.I2C的速度模式

**波特率:**每秒钟传输的位数

|--------|-----|-----------------|----------|
| 标准模式 | Sm | Standard Mode | ≤100kbps |
| 快速模式 | Fm | Fast Mode | ≤400kbps |
| 快速增强模式 | Fm+ | Fast Mode Plus | ≤1Mbps |
| 高速模式 | HSm | High Speed Mode | ≤3.4Mbps |
| 超快模式 | UFm | Ultra Fast Mode | ≤5Mbps |

**注:**STM32只支持标准模式和快速模式

3.5.时钟信号的占空比

快速模式下可以设置时钟信号的占空比

**时钟信号:**SCL传输的信号,控制通信的速率

**注:**多数通信协议使用高电平采样,低电平更新的工作方式

**占空比:**在一个周期中高电压占整个周期的比例

**注:**一般情况下使用2:1的占空比

3.6.I2C模块的初始化

3.6.1.编程接口
cpp 复制代码
void I2C_Init(I2CTypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);

解析:

  • 参数1:I2C名称 I2C1,I2C2
  • 参数2:初始化的参数结构体地址

作用: 初始化I2C,配置I2C的各种参数

  • I2C的波特率
  • I2C的模式
  • I2C的占空比(快速模式)
  • 与从机模式有关的参数

**补充:**I2C_InitTypeDef结构(I2C参数菜单)

cpp 复制代码
typedef struct I2C_InitTypeDef
{
	uint32_t I2C_ClockSpeed;
	uint16_t I2C_Mode;
	uint16_t I2C_DutyCycle;
	uint16_t I2C_Ack;
	uint16_t I2C_OwnAddress1;
	uint16_t I2C_AcknowledgedAddress;
};

分析:

**1.I2C_ClockSpeed:**波特率

  • Sm:<=100
  • Fm:<=400

**2.I2C_Mode:**模式

  • I2C_Mode_I2C:标准I2C模式
  • I2C_Mode_SMBusDevice:系统管理总线设备模式
  • I2C_Mode_SMBusHost:系统管理总线主机模式

**3.I2C_DutyCycle:**快速模式下时钟信号的占空比

  • I2C_DutyCycle_16_9:占空比为16:9
  • I2C_DutyCycle_2:占空比为2:1

**4.I2C_Ack:**与从机模式有关

**5.I2C_OwnAddress1:**与从机模式有关

**6.I2C_AcknowledgedAddress:**用于选择10位从机地址模式

3.6.2.编码部分
cpp 复制代码
//#2:初始化I2C1模块
	
/*开启I2C1模块的时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);
/*施加复位信号*/
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1,ENABLE);
/*释放复位信号*/
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1,DISABLE);
/*I2C结构的前置声明*/
I2C_InitTypeDef I2C_InitStruct;
/*波特率为400k*/
I2C_InitStruct.I2C_ClockSpeed = 400000; 
/*模式为标准I2C*/
I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
/*占空比为2:1*/
I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
/*初始化I2C1*/
I2C_Init(I2C1,&I2C_InitStruct);
/*闭合I2C1总开关*/
I2C_Cmd(I2C1,ENABLE);

注:

  • I2C时钟用的是APB1,而非APB2
  • I2C需要施加和释放复位信号
  • 最后要像串口一样闭合总开关

四、写数据

4.1.I2C模块的内部结构框图

**SDA:**I2C数据引脚

**SCL:**I2C时钟引脚

发送数据:

将发送数据写入发送数据寄存器,I2C模块将数据移到移位寄存器,逐个bit位通过SDA发送

接收数据:

SDA接收从机发送的波形,I2C模块将波形解析,逐个bit位移到移位寄存器,放入接收数据寄存器

**SR1(Status Register1):**状态寄存器1,存放I2C模块运行状态的标志位

**SR2(Status Register2):**状态寄存器2,存放I2C模块运行状态的标志位

**SDA控制电路:**控制SDA引脚向外发送波形,解析SDA接收的波形

**SDA控制寄存器:**控制I2C模块行为

**SCL控制电路:**控制SCL引脚向外发送时钟信号

SCL配置寄存器:配置时钟信号的速度模式,占空比和波特率

4.2.数据发送过程简介

  • 主机发送起始位
  • 主机发送7位地址+读写位
  • 等待从机Ack
  • 主机逐个字节发送数据
  • 每次发送后等待从机Ack
  • 主机发送停止位
4.2.1.发送字节函数
cpp 复制代码
int My_I2C_SendByte(I2C_TypeDef* I2Cx, uint8_t Addr,uint8_t* pData, uint16_t Size);

解析:

  • 参数1:I2C接口的名称
  • 参数2:从机地址,靠左
  • 参数3:要发送的数据
  • 参数4:要发送的数据的数量(单位:字节)
  • 返回值:0 (发送成功)1(寻址失败)2 (发送的数据被拒绝,从机未发送Ack)

**作用:**通过I2C向从机发送若干个字节

4.3.等待总线空闲

总线空闲时,SDA和SCL引脚都为高电平

如果不是高电平时,就需要等待总线空闲

SR2状态寄存器中BUSY(总线忙标志位)

  • 0(RESET):总线空闲
  • 1(SET):总线忙

代码编写:

cpp 复制代码
//#1:等待总线空闲
while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_BUSY) == SET);

**注:**RESET为0,SET为1

4.4.发送起始位

向SDA控制寄存器的START位写1

等待起始位发送完成

SDA配置寄存器中的START(发送起始位)

  • ENABLE:写1
  • DISABLE:写0

SR1状态寄存器中的SB(起始位发送完成标志位)

  • 0(RESET):起始位未发送
  • 1(SET):起始位发送完成

代码编写:

cpp 复制代码
//#2:发送起始位
I2C_GenerateSTART(I2Cx,ENABLE);
//#3:等待起始位发送完成
while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_SB) == RESET);

4.5.发送地址

清零AF标志位,发送7位地址+读写位

等待ADDR从0变成1,变为1后寻址成功,跳出循环

如果AF从0变成1,说明未收到Ack,发送停止位,寻址失败

清零ADDR(先读取SR1,再读取SR2,就可以自动清零)

SR1状态寄存器中的AF(Acknowledge Failure 应答失败标志位)

  • 0(RESET):收到Ack
  • 1(SET):未收到Ack

SR1状态寄存器中的ADDR(寻址标志位)

  • 0(RESET):寻址失败
  • 1(SET):寻址成功

代码编写:

cpp 复制代码
//#3.发送地址
	
/*清除AF标志位*/
I2C_ClearFlag(I2Cx,I2C_FLAG_AF);
/*发送7位地址+读写位*/
I2C_SendData(I2Cx,Addr & 0xfe);
while(1)
{
	/*判断寻址是否成功*/
	if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_ADDR) == SET)
	{
		break;
	}
	/*如果未收到Ack*/
	if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET)
	{
		/*发送停止位*/
		I2C_GenerateSTOP(I2Cx,ENABLE);
		/*寻址失败*/
		return -1;
	}
}
	
//#4:清除ADDR标志位
	
/*读取SR1*/
I2C_ReadRegister(I2Cx,I2C_Register_SR1);
/*读取SR2*/
I2C_ReadRegister(I2Cx,I2C_Register_SR2);

**注:**低位与0表示写

4.6.发送数据

循环发送数据

通过AF标志位,判断上一次发送的数据是否被拒收,如果拒收则发送停止位

通过TxE标志位,判断发送数据寄存器中是否有值,防止造成数据丢失,将数据发出

等待数据发送完成

通过AF标志位,判断上一次发送的数据是否被拒收,如果拒收则发送停止位

通过BTF标志位,判断移位数据寄存器和发送数据寄存器是否都为空

SR1状态寄存器中的BTF(数据发送完成标志位)

  • 0(RESET):数据发送未完成
  • 1(SET):数据发送完成

代码编写:

cpp 复制代码
//#5:发送数据
	
/*循环发送数据*/
for(uint16_t i = 0;i < Size;i++)
{
	while(1)
	{
		/*如果上一次未收到Ack*/
		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)
{
    /*如果上一次未收到Ac*/
	if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET)
	{
		/*发送停止位*/
		I2C_GenerateSTOP(I2Cx,ENABLE);
		/*数据被拒收*/
		return -2;
	}
	/*如果数据发送完成*/
	if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_BTF) == SET)
	{
		/*跳出循环*/
		break;
	}
}

4.7.发送停止位

发送停止位

SDA配置寄存器中的STOP(发送停止位)

  • ENABLE:写1
  • DISABLE:写0

代码编写:

cpp 复制代码
//#5:发送停止位

/*发送停止位*/
I2C_GenerateSTOP(I2Cx,ENABLE);
/*发送成功*/
return 0;

4.8.启动OLED

OLED地址为0x78

cpp 复制代码
uint8_t commands[] = {
0x00,//命令流
0x8d,0x14,//使能电荷泵
0xaf,//打开屏幕开关
0xa5//让屏幕全亮
};

4.9.总代码

cpp 复制代码
#include "stm32f10x.h"

void My_I2C_Init(void);

int My_I2C_SendByte(I2C_TypeDef* I2Cx, uint8_t Addr,uint8_t* pData, uint16_t Size);

int main(void)
{
	My_I2C_Init();
	
	uint8_t commands[] = {0x00,0x8d,0x14,0xaf,0xa5};
	
	My_I2C_SendByte(I2C1,0x78,commands,5);
	
	while(1)
	{
		
	}
}

void My_I2C_Init(void)
{
	//#1:初始化GPIO
	
	/*开启AFIO模块的时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);	
	/*使能I2C1的重映射*/
	GPIO_PinRemapConfig(GPIO_Remap_I2C1,ENABLE);
	/*开启GPIOB模块的时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	/*GPIO结构的前置声明*/
	GPIO_InitTypeDef GPIO_InitStruct;
	/*选择PB8和PB9引脚*/
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
	/*设置复用输出开漏模式*/
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
	/*最大输出速度尾2MHz*/
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
	/*初始化PB8和PB9引脚*/
	GPIO_Init(GPIOB,&GPIO_InitStruct);
	
	//#2:初始化I2C1模块
	
	/*开启I2C1模块的时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);
	/*施加复位信号*/
	RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1,ENABLE);
	/*释放复位信号*/
	RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1,DISABLE);
	/*I2C结构的前置声明*/
	I2C_InitTypeDef I2C_InitStruct;
	/*波特率为400k*/
	I2C_InitStruct.I2C_ClockSpeed = 400000; 
	/*模式为标准I2C*/
	I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
	/*占空比为2:1*/
	I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
	/*初始化I2C1*/
	I2C_Init(I2C1,&I2C_InitStruct);
	/*闭合I2C1总开关*/
	I2C_Cmd(I2C1,ENABLE);
}

int My_I2C_SendByte(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);
	/*发送7位地址+读写位*/
	I2C_SendData(I2Cx,Addr & 0xfe);
	while(1)
	{
		/*判断寻址是否成功*/
		if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_ADDR) == SET)
		{
			break;
		}
		/*如果未收到Ack*/
		if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET)
		{
			/*发送停止位*/
			I2C_GenerateSTOP(I2Cx,ENABLE);
			/*寻址失败*/
			return -1;
		}
	}
	
	//#4:清除ADDR标志位
	
	/*读取SR1*/
	I2C_ReadRegister(I2Cx,I2C_Register_SR1);
	/*读取SR2*/
	I2C_ReadRegister(I2Cx,I2C_Register_SR2);
	
	//#5:发送数据
	
	/*循环发送数据*/
	for(uint16_t i = 0;i < Size;i++)
	{
		while(1)
		{
			/*如果上一次未收到Ack*/
			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)
	{
		/*如果上一次未收到Ac*/
		if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET)
		{
			/*发送停止位*/
			I2C_GenerateSTOP(I2Cx,ENABLE);
			/*数据被拒收*/
			return -2;
		}
		/*如果数据发送完成*/
		if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_BTF) == SET)
		{
			/*跳出循环*/
			break;
		}
	}
	
	//#5:发送停止位
	
	/*发送停止位*/
	I2C_GenerateSTOP(I2Cx,ENABLE);
	/*发送成功*/
	return 0;
}

五、读数据

5.1.接收字节函数

cpp 复制代码
int My_I2C_ReceiveBytes(I2C_TypeDef* I2Cx,uint8_t Addr,uint8_t* pBuffer,uint16_t Size);

解析:

  • 参数1:I2C接口名称
  • 参数2:从机地址,靠左
  • 参数3:接收缓冲区
  • 参数4:要发送的数据的数量(单位:字节)
  • 返回值:0(读取成功)-1(寻址失败)

**作用:**通过I2C从从机读取若干个字节

5.2.数据读取的流程

  • 主机发送起始位
  • 主机发送7位地址+读写位
  • 等待从机Ack
  • 主机逐个字节接收数据
  • 每次接收后等待主机Ack
  • 最后一个字节主机Nak
  • 主机发送停止位

5.3.发送起始位和地址

5.3.1.发送起始位

向SDA控制寄存器的START位写1

等待起始位发送完成

代码编写:

cpp 复制代码
//#1:发送起始位
	
/*发送起始位*/
I2C_GenerateSTART(I2Cx,ENABLE);
/*等待起始位发送完成*/
while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_SB) == RESET);
5.3.2.发送地址+读写位

清零AF标志位,发送7位地址+读写位

等待ADDR从0变成1,变为1后寻址成功,跳出循环

如果AF从0变成1,说明未收到Ack,发送停止位,寻址失败

清零ADDR(先读取SR1,再读取SR2,就可以自动清零)

代码编写:

cpp 复制代码
//#2:发送地址
	
/*清除AF标志位*/
I2C_ClearFlag(I2Cx,I2C_FLAG_AF);
/*发送7位地址+读写位*/
I2C_SendData(I2Cx,Addr | 0x01);
while(1)
{
	/*判断寻址是否成功*/
	if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_ADDR) == SET)
	{
		break;
	}
	/*如果未收到Ack*/
	if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET)
	{
		/*发送停止位*/
		I2C_GenerateSTOP(I2Cx,ENABLE);
		/*寻址失败*/
		return -1;
	}
}

**注:**低位或1表示读

5.4.如何发送ACK和NAK

SDA配置寄存器中的ACK(发送应答位)

  • 发送NAK:写0
  • 发送ACK:写1

**注:**只作用于正在被接收的字节

5.5.如何发送停止位

向SDA配置寄存器中的STOP位写1

**注:**作用于当前字节接收完成之后

5.6.Size = 1

主机发送起始位,7位地址+读写位,从机Ack

清除ADDR标志位,开始接收第一个字节数据

在第一个字节接收完成前写主机NAK和停止位

等待第一个字节数据存储接收数据寄存器(RxNE标志位为1时收到数据),读取数据

代码编写:

cpp 复制代码
//#3:接收数据
if(Size == 1)
{
	/*清除ADDR标志位*/
	/*读取SR1*/
	I2C_ReadRegister(I2Cx,I2C_Register_SR1);
	/*读取SR2*/
	I2C_ReadRegister(I2Cx,I2C_Register_SR2);
	/*ACK = 0,STOP = 1*/
	I2C_AcknowledgeConfig(I2Cx,DISABLE);
	I2C_GenerateSTOP(I2Cx,ENABLE);
	/*等待RxNE为1*/
	while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
	/*读取数据*/
	pBuffer[0] = I2C_ReceiveData(I2Cx);
}

5.7.Size = 2

主机发送起始位,7位地址+读写位,从机Ack

清除ADDR标志位,开始接收第一个字节数据

在第一个字节接收完成前写从机Ack

等待第一个字节数据存储接收数据寄存器(RxNE标志位为1时收到数据),读取数据

在第二个字节接收完成前写主机NAK和停止位

等待第二个字节数据存储接收数据寄存器(RxNE标志位为1时收到数据),读取数据

代码编写:

cpp 复制代码
else if(Size == 2)
{
	/*清除ADDR标志位*/
		
	/*读取SR1*/
	I2C_ReadRegister(I2Cx,I2C_Register_SR1);
	/*读取SR2*/
	I2C_ReadRegister(I2Cx,I2C_Register_SR2);
	/*ACK = 1*/
	I2C_AcknowledgeConfig(I2Cx,ENABLE);
	/*等待RxNE为1*/
	while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
	/*读取第一个字节*/
	pBuffer[0] = I2C_ReceiveData(I2Cx);
	/*ACK = 0,STOP = 1*/
	I2C_AcknowledgeConfig(I2Cx,DISABLE);
	I2C_GenerateSTOP(I2Cx,ENABLE);
    /*等待RxNE为1*/
	while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
	/*读取第二个字节*/
	pBuffer[1] = I2C_ReceiveData(I2Cx);	
}

5.8.Size > 2

主机发送起始位,7位地址+读写位,从机Ack

清除ADDR标志位,开始接收第一个字节数据

在第一个字节接收完成前写从机Ack

等待第一个字节数据存储接收数据寄存器(RxNE标志位为1时收到数据),读取数据

......(重复N - 1次)

在第N个字节接收完成前写主机NAK和停止位

等待第N个字节数据存储接收数据寄存器(RxNE标志位为1时收到数据),读取数据

代码编写:

cpp 复制代码
else
{
	/*清除ADDR标志位*/
		
	/*读取SR1*/
	I2C_ReadRegister(I2Cx,I2C_Register_SR1);
	/*读取SR2*/
	I2C_ReadRegister(I2Cx,I2C_Register_SR2);
	/*ACK = 1*/
	I2C_AcknowledgeConfig(I2Cx,ENABLE);
		
	/*读取第N个字节前的数据*/
		
	for(uint16_t i = 0;i < Size - 1;i++)
	{
		/*等待RxNE为1*/
		while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
		/*读取数据*/
		pBuffer[i] = I2C_ReceiveData(I2Cx);
	}
		
	/*读取第N个字节的数据*/
		
	/*ACK = 0,STOP = 1*/
	I2C_AcknowledgeConfig(I2Cx,DISABLE);
	I2C_GenerateSTOP(I2Cx,ENABLE);
	/*等待RxNE为1*/
	while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
	/*读取第N个字节*/
	pBuffer[Size - 1] = I2C_ReceiveData(I2Cx);
}

/*接收成功*/
return 0

5.9.读取OLED

从OLED中读一个字节,会读出8个bit位数据

其中只有D6位有有效数据,表示屏幕的开关

  • D6 = 0:表示屏幕开启
  • D6 = 1:表示屏幕关闭

代码编写:

cpp 复制代码
uint8_t rcvd;
	
My_I2C_ReceiveBytes(I2C1,0x78,&rcvd,1);
	
/*检测到屏幕亮*/
if((rcvd & (0x01<<6)) == 0)
{
	GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);//亮灯
}
else/*检测到屏幕灭*/
{
	GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);//灭灯
}

解析:

将读取到的值存放在rcvd变量,将rcvd的第六位与1,判断是0还是1

  • 如果是0:Led亮(屏幕开启)
  • 如果是1:Led灭(屏幕关闭)

总代码:

cpp 复制代码
#include "stm32f10x.h"

void My_I2C_Init(void);

int My_I2C_SendByte(I2C_TypeDef* I2Cx, uint8_t Addr,uint8_t* pData, uint16_t Size);

int My_I2C_ReceiveBytes(I2C_TypeDef* I2Cx,uint8_t Addr,uint8_t* pBuffer,uint16_t Size);

void My_OnBoardLED_Init(void);
	
int main(void)
{
	My_I2C_Init();
	
	uint8_t commands[] = {0x00,0x8d,0x14,0xaf,0xa5};
	
	My_I2C_SendByte(I2C1,0x78,commands,5);
	
	My_OnBoardLED_Init();
	
	uint8_t rcvd;
	
	My_I2C_ReceiveBytes(I2C1,0x78,&rcvd,1);
	
	/*检测到屏幕亮*/
	if((rcvd & (0x01<<6)) == 0)
	{
		GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);//亮灯
	}
	else/*检测到屏幕灭*/
	{
		GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);//灭灯
	}
	
	while(1)
	{
		
	}
}

void My_I2C_Init(void)
{
	//#1:初始化GPIO
	
	/*开启AFIO模块的时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);	
	/*使能I2C1的重映射*/
	GPIO_PinRemapConfig(GPIO_Remap_I2C1,ENABLE);
	/*开启GPIOB模块的时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	/*GPIO结构的前置声明*/
	GPIO_InitTypeDef GPIO_InitStruct;
	/*选择PB8和PB9引脚*/
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
	/*设置复用输出开漏模式*/
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
	/*最大输出速度尾2MHz*/
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
	/*初始化PB8和PB9引脚*/
	GPIO_Init(GPIOB,&GPIO_InitStruct);
	
	//#2:初始化I2C1模块
	
	/*开启I2C1模块的时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);
	/*施加复位信号*/
	RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1,ENABLE);
	/*释放复位信号*/
	RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1,DISABLE);
	/*I2C结构的前置声明*/
	I2C_InitTypeDef I2C_InitStruct;
	/*波特率为400k*/
	I2C_InitStruct.I2C_ClockSpeed = 400000; 
	/*模式为标准I2C*/
	I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
	/*占空比为2:1*/
	I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
	/*初始化I2C1*/
	I2C_Init(I2C1,&I2C_InitStruct);
	/*闭合I2C1总开关*/
	I2C_Cmd(I2C1,ENABLE);
}

int My_I2C_SendByte(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);
	/*发送7位地址+读写位*/
	I2C_SendData(I2Cx,Addr & 0xfe);
	while(1)
	{
		/*判断寻址是否成功*/
		if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_ADDR) == SET)
		{
			break;
		}
		/*如果未收到Ack*/
		if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET)
		{
			/*发送停止位*/
			I2C_GenerateSTOP(I2Cx,ENABLE);
			/*寻址失败*/
			return -1;
		}
	}
	
	//#4:清除ADDR标志位
	
	/*读取SR1*/
	I2C_ReadRegister(I2Cx,I2C_Register_SR1);
	/*读取SR2*/
	I2C_ReadRegister(I2Cx,I2C_Register_SR2);
	
	//#5:发送数据
	
	/*循环发送数据*/
	for(uint16_t i = 0;i < Size;i++)
	{
		while(1)
		{
			/*如果上一次未收到Ack*/
			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)
	{
		/*如果上一次未收到Ac*/
		if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET)
		{
			/*发送停止位*/
			I2C_GenerateSTOP(I2Cx,ENABLE);
			/*数据被拒收*/
			return -2;
		}
		/*如果数据发送完成*/
		if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_BTF) == SET)
		{
			/*跳出循环*/
			break;
		}
	}
	
	//#5:发送停止位
	
	/*发送停止位*/
	I2C_GenerateSTOP(I2Cx,ENABLE);
	/*发送成功*/
	return 0;
}

int My_I2C_ReceiveBytes(I2C_TypeDef* I2Cx,uint8_t Addr,uint8_t* pBuffer,uint16_t Size)
{
	//#1:发送起始位
	
	/*发送起始位*/
	I2C_GenerateSTART(I2Cx,ENABLE);
	/*等待起始位发送完成*/
	while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_SB) == RESET);
	
	//#2:发送地址
	
	/*清除AF标志位*/
	I2C_ClearFlag(I2Cx,I2C_FLAG_AF);
	/*发送7位地址+读写位*/
	I2C_SendData(I2Cx,Addr | 0x01);
	while(1)
	{
		/*判断寻址是否成功*/
		if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_ADDR) == SET)
		{
			break;
		}
		/*如果未收到Ack*/
		if(I2C_GetFlagStatus(I2Cx,I2C_FLAG_AF) == SET)
		{
			/*发送停止位*/
			I2C_GenerateSTOP(I2Cx,ENABLE);
			/*寻址失败*/
			return -1;
		}
	}
	
	//#3:接收数据
	if(Size == 1)
	{
		/*清除ADDR标志位*/
		
		/*读取SR1*/
		I2C_ReadRegister(I2Cx,I2C_Register_SR1);
		/*读取SR2*/
		I2C_ReadRegister(I2Cx,I2C_Register_SR2);
		/*ACK = 0,STOP = 1*/
		I2C_AcknowledgeConfig(I2Cx,DISABLE);
		I2C_GenerateSTOP(I2Cx,ENABLE);
		/*等待RxNE为1*/
		while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
		/*读取数据*/
		pBuffer[0] = I2C_ReceiveData(I2Cx);
	}
	else if(Size == 2)
	{
		/*清除ADDR标志位*/
		
		/*读取SR1*/
		I2C_ReadRegister(I2Cx,I2C_Register_SR1);
		/*读取SR2*/
		I2C_ReadRegister(I2Cx,I2C_Register_SR2);
		/*ACK = 1*/
		I2C_AcknowledgeConfig(I2Cx,ENABLE);
		/*等待RxNE为1*/
		while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
		/*读取第一个字节*/
		pBuffer[0] = I2C_ReceiveData(I2Cx);
		/*ACK = 0,STOP = 1*/
		I2C_AcknowledgeConfig(I2Cx,DISABLE);
		I2C_GenerateSTOP(I2Cx,ENABLE);
		/*等待RxNE为1*/
		while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
		/*读取第二个字节*/
		pBuffer[1] = I2C_ReceiveData(I2Cx);
	}
	else
	{
		/*清除ADDR标志位*/
		
		/*读取SR1*/
		I2C_ReadRegister(I2Cx,I2C_Register_SR1);
		/*读取SR2*/
		I2C_ReadRegister(I2Cx,I2C_Register_SR2);
		/*ACK = 1*/
		I2C_AcknowledgeConfig(I2Cx,ENABLE);
		
		/*读取第N个字节前的数据*/
		
		for(uint16_t i = 0;i < Size - 1;i++)
		{
			/*等待RxNE为1*/
			while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
			/*读取数据*/
			pBuffer[i] = I2C_ReceiveData(I2Cx);
		}
		
		/*读取第N个字节的数据*/
		
		/*ACK = 0,STOP = 1*/
		I2C_AcknowledgeConfig(I2Cx,DISABLE);
		I2C_GenerateSTOP(I2Cx,ENABLE);
		/*等待RxNE为1*/
		while(I2C_GetFlagStatus(I2Cx,I2C_FLAG_RXNE) == RESET);
		/*读取第N个字节*/
		pBuffer[Size - 1] = I2C_ReceiveData(I2Cx);
	}
	
	/*接收成功*/
	return 0;
}

void My_OnBoardLED_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStruct;
	
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
	
	GPIO_Init(GPIOC,&GPIO_InitStruct);
}

六、软I2C

6.1.硬I2C VS 软I2C

**硬件I2C(硬件模拟):**靠单片机内部的专用电路自动产生波形

优点:

  • 解放CPU:硬件电路自动处理时序,CPU可以处理其他任务
  • 速度快且稳定:晶振驱动硬件电路,时序精准,适合高速传输
  • 代码简洁:不需要写复杂的延时和操作代码,只需配置好寄存器

缺点:

  • 引脚固定,移植性差
  • 从机没有Ack,主机会卡死
  • 需在外部接上拉电阻

**软件I2C(软件模拟):**靠CPU执行代码控制GPIO引脚高低电平画出波形

优点:

  • 引脚随意:如果I2C的引脚被占用,可以用任意GPIO引脚
  • 兼容性好:不同传感器的I2C时序有差异,软I2C可以进行修改
  • 调试方便:可以清楚看见每个电平变化

缺点:

  • CPU占用高
  • 时序精度一般

6.2.IO引脚初始化

软I2C的GPIO引脚由CPU直接控制,同时为了实现硬I2C的逻辑线与

需要用开漏模式,所以设置为通用输出开漏,且初始电压高(空闲)

代码编写:

cpp 复制代码
void My_SI2C_Init(void)
{
    /*开启GPIOA的时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	
    /*声明GPIO结构变量*/
	GPIO_InitTypeDef GPIO_InitStruct;
	
    /*选择PA0和PA1引脚*/
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
    /*通用输出开漏模式*/
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
    /*最大输出速率为2MHz*/
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
    /*初始化PA0和PA1引脚*/
    GPIO_Init(GPIOA,&GPIO_InitStruct);
    /*将引脚设置位高电平*/
    GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_SET);
	GPIO_WriteBit(GPIOA,GPIO_Pin_1,Bit_SET);
}

6.3.IO读写和延迟函数

6.3.1.编程接口1
cpp 复制代码
void scl_write(uint8_t level);

**作用:**向SCL写0或写1

  • 写0:输出低电平
  • 写1:输出高电平

代码编写:

cpp 复制代码
void scl_write(uint8_t level)
{
	/*向PA0引脚写0*/
	if(level == 0)
	{
		/*输出低电平,总线为低电平*/
		GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_RESET);
	}
	else/*向PA0引脚写1*/
	{
		/*输出高阻态 总线为高电平*/
		GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_SET);
	}
}
6.3.2.编程接口2
cpp 复制代码
void sda_write(uint8_t level);

**作用:**向SDA写0或写1

  • 写0:输出低电平
  • 写1:输出高电平

代码编写:

cpp 复制代码
void sda_write(uint8_t level)
{
	/*向PA1引脚写0*/
	if(level == 0)
	{
		/*输出低电平,总线为低电平*/
		GPIO_WriteBit(GPIOA,GPIO_Pin_1,Bit_RESET);
	}
	else/*向PA1引脚写1*/
	{
		/*输出高阻态 总线为高电平*/
		GPIO_WriteBit(GPIOA,GPIO_Pin_1,Bit_SET);
	}
}
6.3.3.编程接口3
cpp 复制代码
uint8_t sda_read(void);

**作用:**读取SDA的值

  • 返回值为0:低电平
  • 返回值为1:高电平

代码编写:

cpp 复制代码
uint8_t sda_read(void)
{
	/*如果读到PA1为高电平*/
	if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_1) == Bit_SET)
	{
		/*返回1*/
		return 1;
	}
	else/*如果读到PA1为低电平*/
	{
		/*返回0*/
		return 0;
	}
}
6.3.4.编程接口4
cpp 复制代码
void delay_us(uint32_t us)
{
    uint32_t n = us * 8;

    for(uint32_t i = 0;i < n;i++);
}

**作用:**实现微秒级延迟

**注:**每执行一次for循环需要八分之一微秒

6.4.发送起始位和停止位

**起始位:**当SCL为高电平时,SDA产生下降沿

代码编写:

cpp 复制代码
void SendStart(void)
{
	sda_write(0);
	delay_us(1);
}

**停止位:**当SCL为高电平时,SDA产生上升沿

注:

由于停止位前的SDA电平高低不确定,此时若SDA为高电平就无法在停止位产生上升沿

所以需提前拉低SDA,但为了避免时钟线误认为是数据采样,还需要在这之前拉低SCL

所以整个流程为:

  • 拉低SCL,拉低SDA,延迟1us维持电平稳定
  • 拉高SCL,准备接收SDA,延迟1us维持电平稳定
  • 拉高SDA,发送停止位,延迟1us维持电平稳定

代码编写:

cpp 复制代码
void SendStop(void)
{
	scl_write(0);
	sda_write(0);
	delay_us(1);
	scl_write(1);
	delay_us(1);
	sda_write(1);
	delay_us(1);
}

6.5.发送一个字节

发送一个bit位:

拉低SCL,让SDA准备数据0或者1

延迟1us后,再拉高SCL接收数据

最后再延迟1us,维持电平稳定

代码编写:

cpp 复制代码
scl_write(0);
sda_write(x);
delay_us(1);
scl_write(1);
delay_us(1);

发送字节函数:

cpp 复制代码
uint8_t sendByte(uint8_t byte);

解析:

  • 参数1:要发送的字节
  • 返回值:接收方回复,0(ACK)1(NAK)

代码编写:

cpp 复制代码
uint8_t sendByte(uint8_t Byte)
{
	for(int8_t i = 7;i >= 0;i--)
	{
		scl_write(0);
		if((Byte & (0x01 << i)) != 0)
		{
			sda_write(1);
		}
		else
		{
			sda_write(0);
		}
		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();
}

解析:

计算x的值是0还是1:

从Byte数据的高位开始逐位与1

如果与1后的结果是0,说明该bit位数据为0,输入低电平

如果与1后的结果不是0,说明该bit为数据为1,输入高电平

检测ACK或NAK:

拉低SCL,拉高SDA,释放SDA总线,延迟1us稳定电平

如果是接收方ACK,接收方的硬件电路自动拉低SDA引脚电平

如果是接收方NAK,接收方则不作调整,保持SDA高电平

此时再拉高SCL,延迟1us,读取低电平0(ACK),读取高电平1(NAK)

6.6.接收一个字节

接收1个bit位:

拉低SCL,拉高SDA,释放SDA总线,延迟1us稳定电平

如果发送方发送高电平,发送方的硬件电路自动拉低SDA引脚电平

如果发送方发送低电平,发送方则不作调整,保持SDA高电平

此时再拉高SCL,延迟1us,读取低电平0,读取高电平1

代码编写:

cpp 复制代码
scl_write(0);
sda_write(1);
delay_us(1);
scl_write(1);
delay_us(1);
bit = sda_read();

接收字节函数:

cpp 复制代码
uint8_t receiveByte(uint8_t Ack);

解析:

  • 参数1:Ack为0时回复Nak,Ack为1时回复Ack
  • 返回值:读到字节的数据

代码编写:

cpp 复制代码
uint8_t receiveByte(uint8_t Ack)
{
	uint8_t byte = 0;
	for(int8_t i = 7;i >= 0;i--)
	{
		scl_write(0);
		sda_write(1);
		delay_us(1);
		scl_write(1);
		delay_us(1);
		if(sda_read() != 0)
		{
			byte |= 0x01 << i;
		}
    }
		
	//回复ACK或NAK
	scl_write(0);
	sda_write(!Ack);
	delay_us(1);
	scl_write(1);
	delay_us(1);
	return byte;
}

解析:

读出byte数据:

从byte的高位开始逐位或1

  • 如果读取到高电平就或1
  • 如果读到低电平就跳过

回复ACK或NAK:

拉低SCL,控制主机回复应答信号

传参为1代表ACK,传参为0代表NAK

但实际上:

向sda_write()传1时,为高电平代表NAK

向sda_write()传0时,为低电平代表ACK

要对参数ACK取反,才能保证逻辑正确

准备好SDA数据后,延迟1us稳定电平

此时再拉高SCL,延迟1us,读取低电平ACK,读取高电平NAK

注:

与主机回复应答信号不同的是

从机回复ACK或NAK是通过释放SDA

让从机的硬件电路控制选择ACK或NAK

而主机回复应答信号需要CPU主动控制(函数传参)

6.7.综合

软I2C写

cpp 复制代码
int My_SI2C_SendBytes(uint8_t Addr,uint8_t* pData,uint16_t Size);

解析:

  • 参数1:从机地址,靠左
  • 参数2:要发送的数据
  • 参数3:要发送的数据的数量(单位:字节)

代码编写:

cpp 复制代码
int My_SI2C_SendBytes(uint8_t Addr,uint8_t* pData,uint16_t Size)
{
	/*发送起始位*/
	SendStart();
	
	/*发送地址+读写位*/
	
	/*如果没有接收到ACK*/
	if(sendByte(Addr & 0xfe) != 0)
	{
		/*发送停止位*/
		SendStop();
		/*寻址失败*/
		return -1;
	}
	
	for(uint32_t i = 0;i < Size;i++)
	{
		/*如果接收方回复NAK*/
		if(sendByte(pData[i]) != 0)
		{
			/*发送停止位*/
			SendStop();
			/*数据被拒收*/
			return -2;
		}
	}

    /*发送停止位*/
    SendStop();
	/*数据发送成功*/
	return 0;
}

软I2C读

cpp 复制代码
int My_SI2C_ReceiveBytes(uint8_t Addr,uint8_t* pBuffer,uint16_t Size);

解析:

  • 参数1:从机地址,靠左
  • 参数2:接收缓冲区
  • 参数3:要接收的数据的数量(单位:字节)

代码编写:

cpp 复制代码
int My_SI2C_ReceiveBytes(uint8_t Addr,uint8_t* pBuffer,uint16_t Size)
{
	/*发送起始位*/
	SendStart();
	
	/*发送地址+读写位*/
	
	/*如果没有接收到ACK*/
	if(sendByte(Addr | 0x01) != 0)
	{
		/*发送停止位*/
		SendStop();
		/*寻址失败*/
		return -1;
	}
	
	/*接收数据*/
	for(uint32_t i = 0;i < Size - 1;i++)
	{
		pBuffer[i] = receiveByte(1);
	}
	
	pBuffer[Size - 1] = receiveByte(0);
	
	/*发送停止位*/
    SendStop();
    /*数据接收成功*/
	return 0;
}

6.8.测试

总代码编写:

cpp 复制代码
#include "stm32f10x.h"

void My_SI2C_Init(void);
void scl_write(uint8_t level);
void sda_write(uint8_t level);
uint8_t sda_read(void);
void delay_us(uint32_t us);
void SendStart(void);
void SendStop(void);
uint8_t sendByte(uint8_t Byte);
uint8_t receiveByte(uint8_t Ack);
int My_SI2C_SendBytes(uint8_t Addr,uint8_t* pData,uint16_t Size);
int My_SI2C_ReceiveBytes(uint8_t Addr,uint8_t* pBuffer,uint16_t Size);

int main(void)
{
	My_SI2C_Init();
		
	uint8_t commands[] = {
		0x00,//命令流
		0x8d,0x14,//使能电荷泵
		0xaf,//打开屏幕开关
		0xa5,//让屏幕全亮
	};
	
	My_SI2C_SendBytes(0x78,commands,5);
	
	while(1)
	{
	}
}

void My_SI2C_Init(void)
{
	/*开启GPIOA的时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	/*声明GPIO结构变量*/
	GPIO_InitTypeDef GPIO_InitStruct;
	/*选择PA0和PA1引脚*/
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
	/*通用输出开漏模式*/
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
	/*最大输出速率为2MHz*/
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
	/*初始化PA0和PA1引脚*/
  GPIO_Init(GPIOA,&GPIO_InitStruct);
  /*将引脚设置位高电平*/
	GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_SET);
	GPIO_WriteBit(GPIOA,GPIO_Pin_1,Bit_SET);
}

void scl_write(uint8_t level)
{
	/*向PA0引脚写0*/
	if(level == 0)
	{
		/*输出低电平,总线为低电平*/
		GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_RESET);
	}
	else/*向PA0引脚写1*/
	{
		/*输出高阻态 总线为高电平*/
		GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_SET);
	}
}

void sda_write(uint8_t level)
{
	/*向PA1引脚写0*/
	if(level == 0)
	{
		/*输出低电平,总线为低电平*/
		GPIO_WriteBit(GPIOA,GPIO_Pin_1,Bit_RESET);
	}
	else/*向PA1引脚写1*/
	{
		/*输出高阻态 总线为高电平*/
		GPIO_WriteBit(GPIOA,GPIO_Pin_1,Bit_SET);
	}
}

uint8_t sda_read(void)
{
	/*如果读到PA1为高电平*/
	if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_1) == Bit_SET)
	{
		/*返回1*/
		return 1;
	}
	else/*如果读到PA1为低电平*/
	{
		/*返回0*/
		return 0;
	}
}

void delay_us(uint32_t us)
{
    uint32_t n = us * 8;

    for(uint32_t i = 0;i < n;i++);
}

void SendStart(void)
{
	sda_write(0);
	delay_us(1);
}

void SendStop(void)
{
	scl_write(0);
	sda_write(0);
	delay_us(1);
	scl_write(1);
	delay_us(1);
	sda_write(1);
	delay_us(1);
}

uint8_t sendByte(uint8_t Byte)
{
	for(int8_t i = 7;i >= 0;i--)
	{
		scl_write(0);
		if((Byte & (0x01 << i)) != 0)
		{
			sda_write(1);
		}
		else
		{
			sda_write(0);
		}
		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();
}

uint8_t receiveByte(uint8_t Ack)
{
	uint8_t byte = 0;
	for(int8_t i = 7;i >= 0;i--)
	{
		scl_write(0);
		sda_write(1);
		delay_us(1);
		scl_write(1);
		delay_us(1);
		if(sda_read() != 0)
		{
			byte |= 0x01 << i;
		}
	}
		
	//回复ACK或NAK
	scl_write(0);
	sda_write(!Ack);
	delay_us(1);
	scl_write(1);
	delay_us(1);
	return byte;
}

int My_SI2C_SendBytes(uint8_t Addr,uint8_t* pData,uint16_t Size)
{
	/*发送起始位*/
	SendStart();
	
	/*发送地址+读写位*/
	
	/*如果没有接收到ACK*/
	if(sendByte(Addr & 0xfe) != 0)
	{
		/*发送停止位*/
		SendStop();
		/*寻址失败*/
		return -1;
	}
	
	/*发送数据*/
	
	for(uint32_t i = 0;i < Size;i++)
	{
		/*如果接收方回复NAK*/
		if(sendByte(pData[i]) != 0)
		{
			/*发送停止位*/
			SendStop();
			/*数据被拒收*/
			return -2;
		}
	}
	
	/*发送停止位*/
    SendStop();
	/*数据发送成功*/
	return 0;
}

int My_SI2C_ReceiveBytes(uint8_t Addr,uint8_t* pBuffer,uint16_t Size)
{
	/*发送起始位*/
	SendStart();
	
	/*发送地址+读写位*/
	
	/*如果没有接收到ACK*/
	if(sendByte(Addr | 0x01) != 0)
	{
		/*发送停止位*/
		SendStop();
		/*寻址失败*/
		return -1;
	}
	
	/*接收数据*/
	for(uint32_t i = 0;i < Size - 1;i++)
	{
		pBuffer[i] = receiveByte(1);
	}
	
	pBuffer[Size - 1] = receiveByte(0);
	
	/*发送停止位*/
    SendStop();
	/*数据接收成功*/
	return 0;
}

七、封装常用功能

7.1.测试硬件I2C

7.1.1.初始化硬件I2C代码
cpp 复制代码
#include "stm32f10x.h"

void My_I2C1_Init(void);

int main(void)
{
	
	My_I2C1_Init();
	
	while(1)
	{
	}
}

void My_I2C1_Init(void)
{
	//#1:对PB6和PB7进行初始化
	
	/*开启GPIOB的时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	/*声明GPIO结构变量*/
	GPIO_InitTypeDef GPIO_InitStruct;
	/*选择PB6和PB7引脚*/
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
	/*复用输出开漏模式*/
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
	/*最大输出速率为2MHz*/
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
	/*初始化PB6和PB7引脚*/
	GPIO_Init(GPIOB,&GPIO_InitStruct);
	
	//#2:对I2C1进行初始化
	
	/*开启I2C1的时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);
	/*复位I2C1*/
	RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1,ENABLE);
	RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1,DISABLE);
	/*声明I2C结构变量*/
  I2C_InitTypeDef I2C_InitStruct;
	/*波特率为400k*/
	I2C_InitStruct.I2C_ClockSpeed = 400000;
	/*标准I2C模式*/
	I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
	/*占空比为2:1*/
	I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
	/*初始化I2C1*/
	I2C_Init(I2C1,&I2C_InitStruct);
	/*闭合总开关*/
	I2C_Cmd(I2C1,ENABLE);
}
7.1.2.硬件I2C收发数据函数
cpp 复制代码
int My_I2C_SendBytes(...)//发送字节
int My_I2C_ReceiveBytes(...)//接收字节
7.1.3.发送数据
cpp 复制代码
#include "stm32f10x.h"
#include "i2c.h"

void My_I2C1_Init(void);

int main(void)
{
	
	My_I2C1_Init();
	
	uint8_t commands[] = {0x00,0x8d,0x14,0xaf,0xa5};
	
	My_I2C_SendBytes(I2C1,0x78,commands,5);
	
	while(1)
	{
	}
}
7.1.4.接收数据

初始化板载LED

cpp 复制代码
void My_OnBoardLED_Init(void)
{
	/*开启GPIOC的时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
	/*声明GPIO结构变量*/
	GPIO_InitTypeDef GPIO_InitStruct;
	/*选择PC13引脚*/
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
	/*通用输出开漏模式*/
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
	/*最大输出速率为2MHz*/
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;
	/*初始化PC13引脚*/
	GPIO_Init(GPIOC,&GPIO_InitStruct);
	/*默认熄灭*/
	GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);
}

接收数据

cpp 复制代码
#include "stm32f10x.h"
#include "i2c.h"

void My_I2C1_Init(void);

void My_OnBoardLED_Init(void);

int main(void)
{
	
	My_I2C1_Init();
	
	My_OnBoardLED_Init();
	
	uint8_t commands[] = {0x00,0x8d,0x14,0xaf,0xa5};
	
	My_I2C_SendBytes(I2C1,0x78,commands,5);
	
	uint8_t rcvd;
	
	My_I2C_ReceiveBytes(I2C1,0x78,&rcvd,1);
	
	if((rcvd & (0x00 << 6)) == 0)
	{
		GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);
	}
	else
	{
		GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);
	}
	
	while(1)
	{
	}
}

7.2.测试软件I2C

7.2.1.软件I2C收发数据函数
cpp 复制代码
void My_SI2C_Init(...);//初始化软I2C
int My_SI2C_SendBytes(...);//发送字节
int My_SI2C_ReceiveBytes(...);//接收字节
7.2.2.收发数据代码
cpp 复制代码
#include "stm32f10x.h"
#include "si2c.h"

/*声明软I2C结构变量*/
SI2C_TypeDef si2c;

void My_OnBoardLED_Init(void);

int main(void)
{
	/*配置SCL和SDA引脚*/
	si2c.SCL_GPIOx = GPIOB;
	si2c.SCL_GPIO_Pin = GPIO_Pin_6;
	si2c.SDA_GPIOx = GPIOB;
	si2c.SDA_GPIO_Pin = GPIO_Pin_7;
	
	/*初始化软I2C*/
	My_SI2C_Init(&si2c);
	
	My_OnBoardLED_Init();
	
	uint8_t commands[] = {0x00,0x8d,0x14,0xaf,0xa5};
	
	My_SI2C_SendBytes(&si2c,0x78,commands,5);
	
	uint8_t rcvd;
	
	My_SI2C_ReceiveBytes(&si2c,0x78,&rcvd,1);
	
	if((rcvd & (0x00 << 6)) == 0)
	{
		GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);
	}
	else
	{
		GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);
	}
	
	while(1)
	{
	}
}

八、OLED显示器

8.1.OLED显示器的基本原理

通过I2C总线,对OLED中的内存进行读写

内存中的每一个bit位都代表屏幕上的一个像素

  • 写1点亮
  • 写0熄灭

向内存写入数据,调整内存内容,显示对应图像

8.2.屏幕初始化

8.2.1.初始化软I2C
cpp 复制代码
#include "stm32f10x.h"
#include "si2c.h"//引用软I2C的头文件

/*声明软I2C结构变量*/
SI2C_TypeDef si2c;

/*声明软I2C初始化函数*/
void My_SoftwareI2C_Init(void)

int main(void)
{
    /*初始化软I2C*/
	My_SoftwareI2C_Init();

	while(1)
	{
	}
}

void My_SoftwareI2C_Init(void)
{
    /*配置SCL和SDA引脚*/
	
	/*指定SCL的位置*/
	si2c.SCL_GPIOx = GPIOB;
	si2c.SCL_GPIO_Pin = GPIO_Pin_6;
	/*指定SDA的位置*/
	si2c.SDA_GPIOx = GPIOB;
	si2c.SDA_GPIO_Pin = GPIO_Pin_7;
	
	/*初始化软I2C*/
	My_SI2C_Init(&si2c);
}
8.2.2.屏幕初始化代码的编写
cpp 复制代码
int OLED_Init(OLED_TypeDef* OLED,OLED_InitTypeDef* OLED_InitStruct);

解析:

  • 参数1:所使用的OLED的名称
  • 参数2:OLED的初始化参数

**作用:**对OLED显示器进行初始化

**补充:**OLED_InitTypeDef结构(OLED参数菜单)

cpp 复制代码
struct OLED_InitTypeDed
{
    int(*i2c_write_cb)(uint8_t addr,const uint8_t* pdata,uint16_t size);
}

分析

**i2c_write_cb:**I2C写数据回调函数

cpp 复制代码
#include "stm32f10x.h"
#include "si2c.h"//引用软I2C的头文件
#include "oled.h"//引用OLED的头文件

/*声明软I2C结构变量*/
SI2C_TypeDef si2c;

/*声明OLED结构变量*/
OLED_TypeDef oled;

/*声明软I2C初始化函数*/
void My_SoftwareI2C_Init(void);

/*声明软I2C写字节函数*/
int i2c_write_bytes(uint8_t addr,const uint8_t* pdata,uint16_t size);

/*声明OLED初始化函数*/
void My_OLEDScreen_Init(void);

int main(void)
{
  /*初始化软I2C*/
	My_SoftwareI2C_Init();
	/*初始化OLED*/
	My_OLEDScreen_Init();

	while(1)
	{
	}
}

void My_SoftwareI2C_Init(void)
{
    /*配置SCL和SDA引脚*/
	
	/*指定SCL的位置*/
	si2c.SCL_GPIOx = GPIOB;
	si2c.SCL_GPIO_Pin = GPIO_Pin_6;
	/*指定SDA的位置*/
	si2c.SDA_GPIOx = GPIOB;
	si2c.SDA_GPIO_Pin = GPIO_Pin_7;
	
	/*初始化软I2C*/
	My_SI2C_Init(&si2c);
}

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

void My_OLEDScreen_Init(void)
{
	/*声明OLED初始化结构变量*/
	OLED_InitTypeDef OLED_InitStruct;
	/*将软I2C写字节函数赋给结构中的变量*/
	OLED_InitStruct.i2c_write_cb = i2c_write_bytes;
	/*初始化OLED*/
	OLED_Init(&oled,&OLED_InitStruct);
}

解析:

**封装底层驱动:**将软件I2C的发送函数My_SI2C_SendBytes封装成通用接口i2c_write_byte

**注册回调函数:**将i2c_write_byte赋值给OLED初始化结构的i2c_write_cb成员,关联驱动与屏幕

**统一调用入口:**在OLED_Init内部通过回调函数i2c_write_cb发起I2C通信,将参数传给底层发送

8.3.基本概念和操作

8.3.1.画笔和画刷
cpp 复制代码
/*设置画笔*/
void OLED_SetPen(OLED_TypeDef *OLED, uint8_t Pen_Color, uint8_t Width);
/*设置画刷*/
void OLED_SetBrush(OLED_TypeDef *OLED, uint8_t Brush_Color);
8.3.2.屏幕坐标系
8.3.3.光标
cpp 复制代码
/*设置光标位置*/
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);

8.4.文字相关的操作

8.4.1.打印字符串
cpp 复制代码
int OLED_DrawString(OLED_TypeDef* OLED,const char* Str);

解析:

  • 参数1:所使用的OLED的名称
  • 参数2:要显示的字符串

示例:

cpp 复制代码
int main(void)
{
  /*初始化软I2C*/
	My_SoftwareI2C_Init();
	/*初始化OLED*/
	My_OLEDScreen_Init();

	//#1:打印Hello World
	
	/*白色画笔*/
	OLED_SetPen(&oled,PEN_COLOR_WHITE,1);
	/*透明画刷*/
	OLED_SetBrush(&oled,PEN_COLOR_TRANSPARENT);
	/*设置光标*/
	OLED_SetCursor(&oled,24,50);
	/*打印字符串*/
	OLED_DrawString(&oled,"Hello world");
	/*将数据发送OLED*/
	OLED_SendBuffer(&oled);
	
	while(1)
	{
	}
}
8.4.2.设置字体

示例:

cpp 复制代码
#include "stm32f10x.h"
#include "si2c.h"//引用软I2C的头文件
#include "oled.h"//引用OLED的头文件
#include "kt16.h"//引用字体头文件

/*声明软I2C结构变量*/
SI2C_TypeDef si2c;

/*声明OLED结构变量*/
OLED_TypeDef oled;

/*声明软I2C初始化函数*/
void My_SoftwareI2C_Init(void);

/*声明软I2C写字节函数*/
int i2c_write_bytes(uint8_t addr,const uint8_t* pdata,uint16_t size);

/*声明OLED初始化函数*/
void My_OLEDScreen_Init(void);

int main(void)
{
  /*初始化软I2C*/
	My_SoftwareI2C_Init();
	/*初始化OLED*/
	My_OLEDScreen_Init();

	//#1:打印Hello World
	
	/*白色画笔*/
	OLED_SetPen(&oled,PEN_COLOR_WHITE,1);
	/*透明画刷*/
	OLED_SetBrush(&oled,PEN_COLOR_TRANSPARENT);
	/*设置光标*/
	OLED_SetCursor(&oled,24,50);
	/*打印字符串*/
	OLED_DrawString(&oled,"Hello world");
	
	//#2:打印你好世界

	/*设置字体*/
	OLED_SetFont(&oled,&kt16);
	/*计算横坐标*/
	int16_t x = (OLED_GetScreenWidth(&oled) - OLED_GetStrWidth(&oled,"你好世界")) / 2;
	/*设置光标*/
	OLED_SetCursor(&oled,x,28);
	/*打印字符串*/
	OLED_DrawString(&oled,"你好世界");

	/*将数据发送OLED*/
	OLED_SendBuffer(&oled);

	while(1)
	{
	}
}
8.4.3.格式化打印字符串
cpp 复制代码
#include "stm32f10x.h"
#include "si2c.h"//引用软I2C的头文件
#include "oled.h"//引用OLED的头文件
#include "kt16.h"//引用字体头文件

/*声明软I2C结构变量*/
SI2C_TypeDef si2c;

/*声明OLED结构变量*/
OLED_TypeDef oled;

/*声明软I2C初始化函数*/
void My_SoftwareI2C_Init(void);

/*声明软I2C写字节函数*/
int i2c_write_bytes(uint8_t addr,const uint8_t* pdata,uint16_t size);

/*声明OLED初始化函数*/
void My_OLEDScreen_Init(void);

int main(void)
{
  /*初始化软I2C*/
	My_SoftwareI2C_Init();
	/*初始化OLED*/
	My_OLEDScreen_Init();

	//#1:打印Hello World
	
	/*白色画笔*/
	OLED_SetPen(&oled,PEN_COLOR_WHITE,1);
	/*透明画刷*/
	OLED_SetBrush(&oled,PEN_COLOR_TRANSPARENT);
	/*设置光标*/
	OLED_SetCursor(&oled,24,50);
	/*打印字符串*/
	OLED_DrawString(&oled,"Hello world");
	
	//#2:打印你好世界
	
	/*设置字体*/
	OLED_SetFont(&oled,&kt16);
	/*计算横坐标*/
	int16_t x = (OLED_GetScreenWidth(&oled) - OLED_GetStrWidth(&oled,"你好世界")) / 2;
	/*设置光标*/
	OLED_SetCursor(&oled,x,28);
	/*打印字符串*/
	OLED_DrawString(&oled,"你好世界");
	
	//#3:格式化打印字符串
	
	/*设置默认字体*/
	OLED_SetFont(&oled,&default_font);
	/*设置光标*/
	OLED_SetCursor(&oled,58,64);
	/*格式化字符串*/
	OLED_Printf(&oled,"%04d/%02d/%02d",2026,1,29);
	/*将数据发送OLED*/
	OLED_SendBuffer(&oled);
	
	while(1)
	{
	}
}
8.4.4.设置文本区域
cpp 复制代码
/*设置剪切区域*/
void OLED_StartClipRegion(OLED_TypeDef *OLED, int16_t X, int16_t Y, uint16_t Width, uint16_t Height);
/*停止剪切区域*/
void OLED_StopClipRegion(OLED_TypeDef *OLED);
cpp 复制代码
#include "stm32f10x.h"
#include "si2c.h"//引用软I2C的头文件
#include "oled.h"//引用OLED的头文件

/*声明软I2C结构变量*/
SI2C_TypeDef si2c;

/*声明OLED结构变量*/
OLED_TypeDef oled;

/*声明软I2C初始化函数*/
void My_SoftwareI2C_Init(void);

/*声明软I2C写字节函数*/
int i2c_write_bytes(uint8_t addr,const uint8_t* pdata,uint16_t size);

/*声明OLED初始化函数*/
void My_OLEDScreen_Init(void);

int main(void)
{
  /*初始化软I2C*/
	My_SoftwareI2C_Init();
	/*初始化OLED*/
	My_OLEDScreen_Init();

	//#4:设置文本区域
	
	/*设置剪切区域*/
	OLED_StartTextRegion(&oled,0,0,128,64);
	OLED_DrawString(&oled,"We are in the fantasy.\r\n");
	OLED_DrawString(&oled,"Beautiful light is always filled there.\r\n");
	OLED_DrawString(&oled,"The life has been filled...\r\n");
	/*将数据发送OLED*/
	OLED_SendBuffer(&oled);
	
	while(1)
	{
	}
}

8.5.绘图相关的操作

8.5.1.画点
cpp 复制代码
/*画点*/
void OLED_DrawDot(OLED_TypeDef *OLED);
cpp 复制代码
#include "stm32f10x.h"
#include "si2c.h"//引用软I2C的头文件
#include "oled.h"//引用OLED的头文件

/*声明软I2C结构变量*/
SI2C_TypeDef si2c;

/*声明OLED结构变量*/
OLED_TypeDef oled;

/*声明软I2C初始化函数*/
void My_SoftwareI2C_Init(void);

/*声明软I2C写字节函数*/
int i2c_write_bytes(uint8_t addr,const uint8_t* pdata,uint16_t size);

/*声明OLED初始化函数*/
void My_OLEDScreen_Init(void);

int main(void)
{
  /*初始化软I2C*/
	My_SoftwareI2C_Init();
	/*初始化OLED*/
	My_OLEDScreen_Init();

	//#5:画点
	
	/*白色画笔*/
	OLED_SetPen(&oled,PEN_COLOR_WHITE,3);
	/*设置光标*/
	OLED_SetCursor(&oled,29,32);
	/*画点*/
	OLED_DrawDot(&oled);
	
	/*画剩下的7个点*/
	for(uint32_t i = 1;i < 8;i++)
	{
		/*光标左移10像素*/
		OLED_MoveCursorX(&oled,10);
		/*画点*/
		OLED_DrawDot(&oled);
	}
	/*将数据发送OLED*/
	OLED_SendBuffer(&oled);
	
	while(1)
	{
	}
}
8.5.2.画线
cpp 复制代码
/*从光标的当前位置到(x,y)画线*/
void OLED_DrawLine(OLED_TypeDef *OLED, int16_t X, int16_t Y);
/*从光标的当前位置到(x,y)画线,并把光标移动到(x,y)*/
void OLED_LineTo(OLED_TypeDef *OLED, int16_t X, int16_t Y);
cpp 复制代码
#include "stm32f10x.h"
#include "si2c.h"//引用软I2C的头文件
#include "oled.h"//引用OLED的头文件

/*声明软I2C结构变量*/
SI2C_TypeDef si2c;

/*声明OLED结构变量*/
OLED_TypeDef oled;

/*声明软I2C初始化函数*/
void My_SoftwareI2C_Init(void);

/*声明软I2C写字节函数*/
int i2c_write_bytes(uint8_t addr,const uint8_t* pdata,uint16_t size);

/*声明OLED初始化函数*/
void My_OLEDScreen_Init(void);

int main(void)
{
  /*初始化软I2C*/
	My_SoftwareI2C_Init();
	/*初始化OLED*/
	My_OLEDScreen_Init();

	//#6:画线
	
	/*设置光标*/
	OLED_SetCursor(&oled,0,0);
	/*画线*/
	OLED_DrawLine(&oled,128,64);
	
	/*设置光标*/
	OLED_SetCursor(&oled,0,64);
	/*画线*/
	OLED_DrawLine(&oled,128,0);
	
	/*设置光标*/
	OLED_SetCursor(&oled,84,22);
	/*画线*/
	OLED_LineTo(&oled,44,22);
	OLED_LineTo(&oled,44,42);
	OLED_LineTo(&oled,84,42);
	
	/*将数据发送OLED*/
	OLED_SendBuffer(&oled);
	
	while(1)
	{
	}
}
8.5.3.画矩形和画圆
cpp 复制代码
/*画圆*/
void OLED_DrawCircle(OLED_TypeDef *OLED, uint16_t Radius);
/*画矩形*/
void OLED_DrawRect(OLED_TypeDef *OLED, uint16_t Width, uint16_t Height);
cpp 复制代码
#include "stm32f10x.h"
#include "si2c.h"//引用软I2C的头文件
#include "oled.h"//引用OLED的头文件

/*声明软I2C结构变量*/
SI2C_TypeDef si2c;

/*声明OLED结构变量*/
OLED_TypeDef oled;

/*声明软I2C初始化函数*/
void My_SoftwareI2C_Init(void);

/*声明软I2C写字节函数*/
int i2c_write_bytes(uint8_t addr,const uint8_t* pdata,uint16_t size);

/*声明OLED初始化函数*/
void My_OLEDScreen_Init(void);

int main(void)
{
  /*初始化软I2C*/
	My_SoftwareI2C_Init();
	/*初始化OLED*/
	My_OLEDScreen_Init();

	//#7:画矩形和圆
	
	/*设置光标*/
	OLED_SetCursor(&oled,20,20);
	/*白色画笔*/
	OLED_SetPen(&oled,PEN_COLOR_WHITE,1);
	/*透明画刷*/
	OLED_SetBrush(&oled,BRUSH_TRANSPARENT);
	/*画矩形*/
	OLED_DrawRect(&oled,40,20);
	
	/*设置光标*/
	OLED_SetCursor(&oled,65,30);
	/*画圆形*/
	OLED_DrawCircle(&oled,5);
	
	/*设置光标*/
	OLED_SetCursor(&oled,70,20);
	/*透明画笔*/
	OLED_SetPen(&oled,PEN_COLOR_TRANSPARENT,1);
	/*透明画刷*/
	OLED_SetBrush(&oled,BRUSH_WHITE);
	/*画矩形*/
	OLED_DrawRect(&oled,40,20);
	
	/*将数据发送OLED*/
	OLED_SendBuffer(&oled);
	
	while(1)
	{
	}
}
8.5.4.绘制位图
cpp 复制代码
/*画位图*/
void OLED_DrawBitmap(OLED_TypeDef *OLED, uint16_t Width, uint16_t Height, const uint8_t *pBitmap);

图片转换网址:https://javl.github.io/image2cpp/

图像参数设置

满屏为:128 * 64

缩放模式为:stretch to fill canvas

输出格式为:Plain bytes

生成代码

cpp 复制代码
#include "stm32f10x.h"
#include "si2c.h"//引用软I2C的头文件
#include "oled.h"//引用OLED的头文件

/*声明软I2C结构变量*/
SI2C_TypeDef si2c;

/*声明OLED结构变量*/
OLED_TypeDef oled;

/*声明位图数据变量*/
uint8_t bitmap[] = {};
	
/*声明软I2C初始化函数*/
void My_SoftwareI2C_Init(void);

/*声明软I2C写字节函数*/
int i2c_write_bytes(uint8_t addr,const uint8_t* pdata,uint16_t size);

/*声明OLED初始化函数*/
void My_OLEDScreen_Init(void);

int main(void)
{
  /*初始化软I2C*/
	My_SoftwareI2C_Init();
	/*初始化OLED*/
	My_OLEDScreen_Init();

	//#8:绘制位图
	
	/*设置光标*/
	OLED_SetCursor(&oled,0,0);
	/*绘图*/
	OLED_DrawBitmap(&oled,128,64,bitmap);
	
	/*将数据发送OLED*/
	OLED_SendBuffer(&oled);
	
	while(1)
	{
	}
}

绘制动图:

cpp 复制代码
#include "stm32f10x.h"
#include "si2c.h"//引用软I2C的头文件
#include "oled.h"//引用OLED的头文件
#include "delay.h"

/*声明软I2C结构变量*/
SI2C_TypeDef si2c;

/*声明OLED结构变量*/
OLED_TypeDef oled;

/*声明位图数据变量*/
uint8_t bitmap[] = {};
const uint8_t bitmapFlip[] = {};
	
/*声明软I2C初始化函数*/
void My_SoftwareI2C_Init(void);

/*声明软I2C写字节函数*/
int i2c_write_bytes(uint8_t addr,const uint8_t* pdata,uint16_t size);

/*声明OLED初始化函数*/
void My_OLEDScreen_Init(void);

int main(void)
{
    /*初始化软I2C*/
	My_SoftwareI2C_Init();
	/*初始化OLED*/
	My_OLEDScreen_Init();

	//#8:绘制动图
	
	while(1)
	{
		/*清屏*/
		OLED_Clear(&oled)
		/*设置光标*/
		OLED_SetCursor(&oled,0,0);
		/*绘图*/
	    OLED_DrawBitmap(&oled,128,64,bitmap);
		/*将数据发送OLED*/
	    OLED_SendBuffer(&oled);
		
		Delay(500);
		
		/*清屏*/
		OLED_Clear(&oled)
		/*设置光标*/
		OLED_SetCursor(&oled,0,0);
		/*绘图*/
	    OLED_DrawBitmap(&oled,128,64,bitmap);
		/*将数据发送OLED*/
	    OLED_SendBuffer(&oled);
		
		Delay(500);
	}
}
相关推荐
阳光宅男@李光熠2 小时前
【电子通识】锅仔片类型、规格与应用选择指南
笔记·学习
仰泳之鹅2 小时前
【杂谈】stm32重定向printf为什么需要勾选MicroLIB
stm32·单片机·嵌入式硬件
小六花s2 小时前
SQL注入笔记
数据库·笔记·sql
菜菜小狗的学习笔记2 小时前
黑马程序员java web学习笔记--后端进阶(一)AOP
java·笔记·学习
码农三叔2 小时前
(7-3-01)电机与执行器系统:驱动器开发与控制接口(1)电机驱动电路+编码器与反馈
人工智能·单片机·嵌入式硬件·架构·机器人·人形机器人
wdfk_prog3 小时前
[Linux]学习笔记系列 -- [drivers][clk]clk-bulk
linux·笔记·学习
charlie1145141913 小时前
机器学习概论:一门教计算机如何“不确定地正确”的学问
人工智能·笔记·机器学习·工程实践
三佛科技-134163842123 小时前
HN3401_P沟道-30V -4.2A场效应管MOSFET应用场景分析
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
三佛科技-187366133973 小时前
宠物泡泡机方案开发,宠物洗澡打泡机方案
单片机·嵌入式硬件