文章目录
1.I2C基本电路结构
I2C 通信中,SDA(数据线)和 SCL(时钟线)都是采用开漏(open-drain/漏极开路)模式来驱动的,主要原因如下:
避免总线冲突:I2C 总线是多主多从结构,任何一个设备都可以拉低(驱动低电平)总线,但没有设备能主动拉高总线(只能通过上拉电阻实现高电平)。如果一个设备输出高电平、另一个设备输出低电平,就可能导致短路冲突。开漏输出可以避免这种冲突,因为开漏模式下只有拉低信号,不会主动拉高信号。
信号同步:由于 I2C 总线的开漏特性,多个设备可以同时连接在 SDA 和 SCL 上,各设备只需在需要时拉低信号,而上拉电阻会自动将信号恢复为高电平。这种设计使得多个设备可以同步工作,同时保证总线的安全性和稳定性。
复用功能:在很多微控制器(如 STM32)中,I2C 的 SDA 和 SCL 引脚通常通过设置为“开漏复用输出”来实现多功能配置,即既可以作为普通 GPIO 使用,也可以通过复用功能来作为 I2C 通信端口。这使得一个引脚可以兼顾多种功能,增加了芯片的灵活性。
2.I2C通信协议
- I2C通信的基本流程
- I2C的数据帧格式
- 起始位和停止位
- 寻址
- 传输数据
- 示例
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。
具体实现
- 等待总线空闲—判断是否位SR2寄存器的BUSY
- 发送起始位—SB为0发送未完成,1 为发送完成。SB位硬件置0。
- 发送地址。—清除AF, 发送地址。判断发送成功后跳出。ADDR==1。
- 清除地址ADDR。读SR1,读SR2。
- 发送数据— 判断上一个数据是否发送完成,且没有收到AF=1.
- 发送停止位----移位寄存器为空,发送数据寄存器为空—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.绘制位图
如何准备位图:点击这里