模拟I2C/IIC协议
I2C/IIC协议数据格式
- IIC协议说明书,提取码:vb9f
- 废话不多说,直接上图,IIC基本传输格式
- 这里以STM32的GPIO口来模拟,其它都是同理的。
- 注意:
- I2C通信仅在SCL处于高电平有效
- I2C发送数据位从高到低(MSB->LSB)
起始信号
- 起始信号如图所示。在SCL为高电平的时候,SDA从高到低,作为起始信号。
- SCL = 1
- SDA = 1
- SCL = 1(SDA,SCL均高)
- SDA = 0 (触发起始信号)
/**
* IIC起始信号
* SCL 高电平
* SDA 高->低
**/
void IIC_Start()
{
SDA_OUT(); // SDA输出模式
IIC_delay_us();
/* 拉低时钟和总线 */
SCL(0);
SDA(0);
IIC_delay_us();
/* 拉高总线 和 时钟 */
SDA(1);
SCL(1);
IIC_delay_us();
/* 拉低总线 */
SDA(0);
IIC_delay_us();
/* 拉低时钟 准备发送数据 */
SCL(0);
}
发送数据
I2C Header
建立起始信号后,首先发送 从机地址 + 读/写,从机地址多数为7bit(此外有10bit),读写标志位1bit(写为0/读为1)
)
以SHT30-DIS温湿度传感器为例
SHT30的默认从机地址:0x44
本来按4bit分割: 0x44 = 0b 0100 0100
因为是7bit的地址,所以实际上0x44 = 0b 0100 0100 = 0b 100 0100
最高位的0是不用出现的
I2C一次发生的1字节8bit,从机地址只有7bit,这显然不够,所以最低位就是读写标志位了
-
从机地址+写 => 0x44<<1 | 0x00 = 0x80
0x44 => 0b 0100 0100
0x44 << 1 => 0b 1000 1000 = 0x80
在利用或运算:与0x00或运算可以得到最低位为0(注意由于从机地址左移1bit所以最低位不可能是1,放心大胆去做)
-
从机地址+读 => 0x44<<1 | 0x01 = 0x89
利用或运算:与0x01或运算可以得到最低位为1
0x80 => 0b 1000 1000
|
0x01 => 0b 0000 0001
结果=> 0 b1000 1001 = 0x89
可以先提前把地址定义好
Send One Byte
- SCL = 0,准备SDA(0/1)数据
- SCL = 1 ,发送SDA(0/1)数据
- 一次发送 一个字节 8bit
- for(i=0;i<8;i++)
- SCL = 0 准备数据
- 取出最高位
例如 TXData = 0xA6 = 0b 1010 0110
取出最高位MSB:
0xA6 => 0b 1 010 0110
(按位与)&
0x80 => 0b 1 000 0000
最后结果 0b1000 0000(0x80)
结果向右移动7位
0b1000 0000 >> 7 = 0b 0000 0001 = 0x01(取出第最高位付给SDA)
4.再把要发送的数据左移一位
例如之前0xA6 取出最高位MSB后,下一次应该发送0xA6中的第MSB-1位
0b 1010 0110 << 1 = 0b 0100 1100
这样就把要取的数据放到MSB了 - SCL = 1 发送数据
/**
* 主机发送一字节的数据
* SCL 高电平 SDA输出模式(SDA 高为1,低为0)
* SCL 低电平 准备数据
**/
void IIC_SendOneByte(uint8_t SendData)
{
/* 拉低时钟,SDA输出模式 */
SCL(0);
SDA_OUT();
IIC_delay_us();
/* 发送数据 */
for (size_t i = 0; i < 8; i++)
{
/* 准备数据,从最高位发送 */
if((SendData & 0x80)>>7)
SDA(1);
else
SDA(0);
SendData <<= 1;
/* 拉高时钟,发送数据 */
SCL(1);
IIC_delay_us();
/* 拉低时钟,准备数据 */
SCL(0);
}
/* 拉低时钟,准备接收ACK*/
SCL(0);
}
应答信号ACK/NACK
在主机发送数据后,从机接收后需要给主机一个响应信号,让主机确认是否发送数据成功,从机是否接受成功。
ACK/NACK
- 主机发完数据后,引脚改为输入模式,读取从机响应的应答信号
- ACK : 拉高SCL ,读取SDA ,SDA为低电平则为有效响应
- NACK: 拉高SCL ,读取SDA ,SDA为高电平则为无效响应
从机响应ACK/NACK
- 发完数据,SCL = 0
- 修改SDA输入模式 (准备接受应答信号)
- 等待SDA = 1 (注意超时判断)
/**
* 主机发送命令等待从机ACK回应
* SCL 高电平
* SDA输入模式,等待SDA响应
* (SDA 高无效,低有效)
**/
uint8_t IIC_Wait_ACK()
{
/* 先拉低时钟,SDA改为输入模式 */
SCL(0);
SDA_INPUT();
IIC_delay_us();
/* 拉高SCL,准备接收数据 */
SCL(1);
/* 等待从机响应 */
uint8_t timeout = 0;
while (SDA_Read)
{
if (timeout > 10)
{
return ACK_ERROR;
}
timeout++;
IIC_TimeOut();
}
SCL(0);
return ACK_OK;
}
主机发送ACK/NACK
主机发送ACK/NACK两种情况:
1.主机发送ACK
主机发送从机读命令,从机响应后需要向主机发送数据,此时主机接收数据后需要回应从机,因此需要主动发送ACK,告诉从机完成接收数据。
- 拉低时钟,SCL = 0
- SDA输出模式(准备产生SDA信号)
- SDA = 0 (应答ACK)
- 拉高时钟,SCL =1 ,发送ACK
/**
* 主机接收从机数据,回应从机ACK
* SCL 高电平
* SDA输出模式(SDA 高无效,低有效)
**/
void IIC_ACK()
{
/* 先拉低时钟,SDA输出模式 */
SCL(0);
SDA_OUT();
/* 准备ACK 拉低SDA */
SDA(0);
IIC_delay_us();
/* 拉高时钟 */
SCL(1);
IIC_delay_us();
/* 再拉低时钟,结束第九脉冲 */
SCL(0);
IIC_delay_us();
}
2.主机发送NACK
当主机接收完数据,不再打算接收数据,需要回应从机不再发送数据,因此需要主动发送NACK。
- 拉低时钟,SCL = 0
- SDA输出模式(准备产生SDA信号)
- SDA = 1 (应答ACK)
- 拉高时钟,SCL =1 ,发送NACK
/**
* 主机接收从机数据,回应从机NOACK
* SCL 高电平
* SDA输出模式(SDA 高无效,低有效)
**/
void IIC_NACK()
{
/* 先拉低时钟,SDA输出模式 */
SCL(0);
SDA_OUT();
/* 准备ACK 拉高SDA */
SDA(1);
IIC_delay_us();
/* 拉高时钟 */
SCL(1);
IIC_delay_us();
/* 再拉低时钟,结束第九脉冲 */
SCL(0);
IIC_delay_us();
}
读取数据
数据读取参考数据发送的实现,由于I2C是以发高位到低位(MSB->LSB),可以每次用最低位接收数据,接收数据后再左移1bit,再继续这样接收下去,循环直至接收完整的8bit
- 拉低时钟,SCL = 0
- 左移1bit (准备接收数据)
- 拉高时钟,SCL = 1
- 读取SDA的值(与0x01或运算可以实现最低位置1)
若读到的是0,可以省略与0x10做与运算,左移空出来的就是0
/**
* 主机接收一字节的数据
* SCL 高电平 SDA输出模式(SDA 高为1,低为0)
**/
uint8_t IIC_ReadOneByte()
{
uint8_t RecData = 0;
/* 拉低时钟,SDA输入模式 */
SCL(0);
SDA_INPUT();
IIC_delay_us();
/* 接收数据 */
for (size_t i = 0; i < 8; i++)
{
/* 拉低时钟,准备接收下一位 */
SCL(0);
IIC_delay_us();
/* 拉高时钟,从最高位接收 */
SCL(1);
/* 空出最低位,用来接收数据 */
RecData <<= 1;
if (SDA_Read)
RecData |= 0x01;
IIC_delay_us();
}
return RecData;
}
停止信号
停止信号是在SCL = 1,SDA从到低拉高(0=>1),由主机产生的停止信号
- SCL = 0
- SDA = 0
- SCL = 1
- SDA = 1 (触发停止)
/**
* IIC停止信号
* SCL 高电平
* SDA 低->高
**/
void IIC_Stop()
{
/* 先拉低时钟,SDA输出模式 */
SCL(0);
SDA_OUT();
IIC_delay_us();
/* 拉低时钟和总线 */
SCL(0);
SDA(0);
IIC_delay_us();
/* 先拉高时钟 */
SCL(1);
IIC_delay_us();
/* 拉高总线 等待停止ACK*/
SDA(1);
IIC_delay_us();
SCL(0);
}
关于I2C通讯时间,电平要求
实现代码
myiic.h
#ifndef MYIIC_H
#define MYIIC_H
#include "main.h"
/**
* SDA PB9
* SCL PB8
*/
#define SDA_PORT GPIOB
#define SCL_PORT GPIOB
#define SDA_PIN GPIO_PIN_9
#define SCL_PIN GPIO_PIN_8
/**
* SDA SCL
* 输出操作(追求效率可以直接操作寄存器)
*/
#define SCL(n) (n==1?HAL_GPIO_WritePin(SCL_PORT,SCL_PIN,GPIO_PIN_SET):HAL_GPIO_WritePin(SCL_PORT,SCL_PIN,GPIO_PIN_RESET))
#define SDA(n) (n==1?HAL_GPIO_WritePin(SDA_PORT,SDA_PIN,GPIO_PIN_SET):HAL_GPIO_WritePin(SDA_PORT,SDA_PIN,GPIO_PIN_RESET))
/**
* SDA
* 读取操作
**/
#define SDA_Read HAL_GPIO_ReadPin(SDA_PORT,SDA_PIN)
/**
* ACK 回应状态
**/
#define ACK_OK 0
#define ACK_ERROR 1
void IIC_Init(); //IIC初始化
void IIC_Start(); //IIC起始信号
void IIC_Stop(); //IIC停止信号
uint8_t IIC_Wait_ACK(); //IIC主机等待从机响应ACK
void IIC_ACK(); //IIC主机向从机发送ACK
void IIC_NACK(); //IIC主机向从机发送NOACK
void IIC_SendOneByte(uint8_t SendData); //IIC主机发送
uint8_t IIC_ReadOneByte(); //IIC从机接收
#endif
myiic.c
#include "myiic.h"
void SDA_OUT(); //SDA输出模式
void SDA_INPUT(); //SDA输入模式
void IIC_TimeOut(); //IIC 超时
void IIC_delay_us(); //IIC delay
/* IIC GPIO PIN 初始化 */
void IIC_Init()
{
/* 开GPIO模拟口的时钟 */
__HAL_RCC_GPIOB_CLK_ENABLE();
/* 初始化 SCL */
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = SCL_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(SCL_PORT, &GPIO_InitStruct);
/* 初始化 SDA */
SDA_OUT();
}
/**
* IIC起始信号
* SCL 高电平
* SDA 高->低
**/
void IIC_Start()
{
SDA_OUT(); // SDA输出模式
IIC_delay_us();
/* 拉低时钟和总线 */
SCL(0);
SDA(0);
IIC_delay_us();
/* 拉高总线 和 时钟 */
SDA(1);
SCL(1);
IIC_delay_us();
/* 拉低总线 */
SDA(0);
IIC_delay_us();
/* 拉低时钟 准备发送数据 */
SCL(0);
}
/**
* IIC停止信号
* SCL 高电平
* SDA 低->高
**/
void IIC_Stop()
{
/* 先拉低时钟,SDA输出模式 */
SCL(0);
SDA_OUT();
IIC_delay_us();
/* 拉低时钟和总线 */
SCL(0);
SDA(0);
IIC_delay_us();
/* 先拉高时钟 */
SCL(1);
IIC_delay_us();
/* 拉高总线 等待停止ACK*/
SDA(1);
IIC_delay_us();
SCL(0);
}
/**
* 主机发送命令等待从机ACK回应
* SCL 高电平
* SDA输入模式,等待SDA响应
* (SDA 高无效,低有效)
**/
uint8_t IIC_Wait_ACK()
{
/* 先拉低时钟,SDA改为输入模式 */
SCL(0);
SDA_INPUT();
IIC_delay_us();
/* 拉高SCL,准备接收数据 */
SCL(1);
/* 等待从机响应 */
uint8_t timeout = 0;
while (SDA_Read)
{
if (timeout > 10)
{
return ACK_ERROR;
}
timeout++;
IIC_TimeOut();
}
SCL(0);
return ACK_OK;
}
/**
* 主机接收从机数据,回应从机ACK
* SCL 高电平
* SDA输出模式(SDA 高无效,低有效)
**/
void IIC_ACK()
{
/* 先拉低时钟,SDA输出模式 */
SCL(0);
SDA_OUT();
/* 准备ACK 拉低SDA */
SDA(0);
IIC_delay_us();
/* 拉高时钟 */
SCL(1);
IIC_delay_us();
/* 再拉低时钟,结束第九脉冲 */
SCL(0);
IIC_delay_us();
}
/**
* 主机接收从机数据,回应从机NOACK
* SCL 高电平
* SDA输出模式(SDA 高无效,低有效)
**/
void IIC_NACK()
{
/* 先拉低时钟,SDA输出模式 */
SCL(0);
SDA_OUT();
/* 准备ACK 拉高SDA */
SDA(1);
IIC_delay_us();
/* 拉高时钟 */
SCL(1);
IIC_delay_us();
/* 再拉低时钟,结束第九脉冲 */
SCL(0);
IIC_delay_us();
}
/**
* 主机发送一字节的数据
* SCL 高电平 SDA输出模式(SDA 高为1,低为0)
* SCL 低电平 准备数据
**/
void IIC_SendOneByte(uint8_t SendData)
{
/* 拉低时钟,SDA输出模式 */
SCL(0);
SDA_OUT();
IIC_delay_us();
/* 发送数据 */
for (size_t i = 0; i < 8; i++)
{
/* 准备数据,从最高位发送 */
if((SendData & 0x80)>>7)
SDA(1);
else
SDA(0);
SendData <<= 1;
/* 拉高时钟,发送数据 */
SCL(1);
IIC_delay_us();
/* 拉低时钟,准备数据 */
SCL(0);
}
/* 拉低时钟,准备接收ACK*/
SCL(0);
}
/**
* 主机接收一字节的数据
* SCL 高电平 SDA输出模式(SDA 高为1,低为0)
**/
uint8_t IIC_ReadOneByte()
{
uint8_t RecData = 0;
/* 拉低时钟,SDA输入模式 */
SCL(0);
SDA_INPUT();
IIC_delay_us();
/* 接收数据 */
for (size_t i = 0; i < 8; i++)
{
/* 拉低时钟,准备接收下一位 */
SCL(0);
IIC_delay_us();
/* 拉高时钟,从最高位接收 */
SCL(1);
/* 空出最低位,用来接收数据 */
RecData <<= 1;
if (SDA_Read)
RecData |= 0x01;
IIC_delay_us();
}
return RecData;
}
//SDA输出模式
void SDA_OUT()
{
/* 初始化 SDA */
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = SDA_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);
}
//SDA输入模式
void SDA_INPUT()
{
/* 初始化 SDA */
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = SDA_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
//上拉是因为通常我们只是用来等待从机拉低SDA,产生ACK
//当然也可以靠上拉电阻拉高总线
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);
}
void IIC_delay_us()
{
HAL_Delay_us(3);
}
void IIC_TimeOut()
{
HAL_Delay_us(2);
}
完