I2C协议之软件模拟(一)

本文详细介绍了I2C协议的原理,包括总线空闲状态、起始和停止条件、数据有效性、字节格式、响应ACK信号、从设备地址选择及数据方向。并针对STM8L平台提供了模拟I2C主设备的C代码实现,强调了在模拟过程中的注意事项,如正确生成起始和停止信号,避免SCL下降沿引发误操作,以及确保数据传输的稳定性。下篇将通过SHT20温湿度传感器的应用实例进一步阐述。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

I2C(Inter-Integrated Circuit)协议是一种用于同步、半双工、串行总线(由SCL时钟线、SDA数据线组成)上的协议。规定了总线空闲状态起始条件停止条件数据有效性字节格式响应ACK信号从设备地址选择数据方向。有主从机之分,主机master就是掌控SCL时钟信号的一方,并且起始信号和停止信号也由主机发送。

一、协议说明

(1)总线空闲状态:SCL、SDA均为高电平,此时主从设备都不控制总线(主从设备的SDA和SCL引脚为输入或者开漏输出),由外部上拉电阻将总线拉高。

(2)起始条件:SCL在高电平的时候,SDA出现下降沿,如下图:
在这里插入图片描述

(3)停止条件:SCL在高电平的时候,SDA出现上升沿,如下图:
在这里插入图片描述

(4)数据有效性:什么时候读数据(读SDA电平)有效?在SCL为高电平时,读数据有效。什么时候写数据(改变SDA电平)有效?在SCL为低电平时,写数据有效。读写时序如下图:
在这里插入图片描述

(5)字节格式:发送到SDA线上的每个字节必须为8位,首先传输的时字节最高位(MSB)。

(6)响应ACK:为确保发送数据被对方可靠地接收,接收方必须发出ACK信号表示已成功接收数据。这个ACK信号就是接收方在接收完一个字节数据后,SCL的第一个下降沿处拉低SDA。发送方如何接收这个ACK?在发送方发送完一个字节数据后,SCL的第一个高电平处读取SDA线(SDA引脚处于输入状态不会使SDA拉低),如果读出为低,则表示接收方成功接收到数据。在发送方读取完ACK后,接收方需要释放SDA,即在SCL的第二个下降沿处将SDA设置为开漏输出,释放SDA总线

(7)从设备地址选择和数据方向:在发送起始信号之后,紧接着发送的一个字节,其前7位是从设备的地址,最后一位表示数据传输的方向,0表示写(即接下来由主机发送数据),1表示读(即接下来由主机接收数据)。

二、C代码实现

下面模拟的时I2C master主机的时序。SCL信号由主机发出。
(1)模拟I2C使用到的IO口配置和延时函数,与平台相关(以STM8L为例)

//SCL引脚配置为输出
#define SCL_SET_OUTPUT()        GPIO_Init(I2C_SCL_GROUP, I2C_SCL_PIN, GPIO_Mode_Out_OD_HiZ_Fast)
//SCL引脚配置为输入
#define SCL_SET_INPUT()         GPIO_Init(I2C_SCL_GROUP, I2C_SCL_PIN, GPIO_Mode_In_FL_No_IT)

//SDA引脚配置为输出
#define SDA_SET_OUTPUT()        GPIO_Init(I2C_SDA_GROUP, I2C_SDA_PIN, GPIO_Mode_Out_OD_HiZ_Fast)
//SDA引脚配置为输入
#define SDA_SET_INPUT()         GPIO_Init(I2C_SDA_GROUP, I2C_SDA_PIN, GPIO_Mode_In_FL_No_IT)

//读SCL引脚电平
#define SCL_READ()              GPIO_ReadInputDataBit(I2C_SCL_GROUP, I2C_SCL_PIN)
//读SDA引脚电平
#define SDA_READ()              GPIO_ReadInputDataBit(I2C_SDA_GROUP, I2C_SDA_PIN)

//SCL引脚拉高
#define SCL_WRITE_HIGH()        GPIO_SetBits(I2C_SCL_GROUP, I2C_SCL_PIN)
//SCL引脚拉低
#define SCL_WRITE_LOW()         GPIO_ResetBits(I2C_SCL_GROUP, I2C_SCL_PIN)

//SDA引脚拉高
#define SDA_WRITE_HIGH()        GPIO_SetBits(I2C_SDA_GROUP, I2C_SDA_PIN)
//SDA引脚拉低
#define SDA_WRITE_LOW()         GPIO_ResetBits(I2C_SDA_GROUP, I2C_SDA_PIN)

//延时的目的是I2C设备由上升沿和下降沿时间,延时周期一般大于1/400KHz即可。该延时也决定了数据传输的速率
void delay_us(uint8_t delay_time)
{
    while(delay_time--)
    {
        nop();
    }
}

(2)起始信号

void i2c_start(void)
{
    //对SCL和SDA进行输出配置
    SDA_SET_OUTPUT();
    delay_us(DELAY_CNT);

    //拉高SCL和SDA
    SDA_WRITE_HIGH();
    delay_us(DELAY_CNT);

    SCL_WRITE_HIGH();
    delay_us(DELAY_CNT);

    //在SCL高电平时,SDA出现下降沿,即触发开始信号
    SDA_WRITE_LOW();
    delay_us(DELAY_CNT);
}

(3)停止信号

void i2c_stop(void)
{
    //对SCL和SDA进行输出配置
    SDA_SET_OUTPUT();
    delay_us(DELAY_CNT);

    //拉高SCL,拉低SDA
    SDA_WRITE_LOW();
    delay_us(DELAY_CNT);

    SCL_WRITE_HIGH();
    delay_us(DELAY_CNT);

    //在SCL高电平时,SDA出现上升沿,即触发停止信号
    SDA_WRITE_HIGH();
    delay_us(DELAY_CNT);
}

(4)发送数据

/*
 * 返回值  	0:没有收到接收方的ACK信号(此时需要重发数据或者停止传输)
 *         	1:收到了接收方的ACK信号
 */
void i2c_send(uint8_t send_data)
{
    int8_t send_data_index = 0;

    for(send_data_index = 7; send_data_index >= 0; send_data_index--)
    {
        //拉低SCL,改变SDA
        SCL_WRITE_LOW();
        delay_us(DELAY_CNT);

        if(send_data_index == 7)
        {
            SDA_SET_OUTPUT();
            delay_us(DELAY_CNT);
        }

        if(send_data >> send_data_index & 0x01)
        {
            SDA_WRITE_HIGH();
            delay_us(DELAY_CNT);
        }
        else
        {
            SDA_WRITE_LOW();
            delay_us(DELAY_CNT);
        }

        //拉高SCL,保持SDA稳定,供从设备读取
        SCL_WRITE_HIGH();
        delay_us(DELAY_CNT);
    }

    //拉低SCL,触发从设备ACK
    SCL_WRITE_LOW();
    delay_us(DELAY_CNT);
}

(5)等待ACK信号

uint8_t i2c_wait_ack(void)
{
    uint8_t ack = 0;

    SDA_SET_INPUT();
    delay_us(DELAY_CNT);

    //读SDA,检测从设备是否回应
    SCL_WRITE_HIGH();
    delay_us(DELAY_CNT);

    ack = (SDA_READ() != RESET) ? 0 : 1;

    SCL_WRITE_LOW();
    delay_us(DELAY_CNT);

    return ack;
}

(6)接收数据

void i2c_receive(uint8_t *p_receive_buff)
{
    int8_t send_data_index = 0;

    for(send_data_index = 7; send_data_index >= 0; send_data_index--)
    {
        //拉低SCL,供从设备改变SDA
        SCL_WRITE_LOW();
        delay_us(DELAY_CNT);

        if(send_data_index == 7)
        {
            SDA_SET_INPUT();
            delay_us(DELAY_CNT);
        }

        //拉高SCL,读取SDA
        SCL_WRITE_HIGH();
        delay_us(DELAY_CNT);
        if(SDA_READ() != RESET)
        {
            (*p_receive_buff) |= (0x01 << send_data_index);
        }
        else
        {
            (*p_receive_buff) &= ~(0x01 << send_data_index);
        }
    }
}

(7)发送ACK信号

void i2c_send_ack(void)
{
    //读完数据,在第九个时钟(即第八个时钟的下降沿)发出ACK信号
    SCL_WRITE_LOW();
    delay_us(DELAY_CNT);

    SDA_SET_OUTPUT();
    delay_us(DELAY_CNT);

    SDA_WRITE_LOW();
    delay_us(DELAY_CNT);

    //释放总线
    SCL_WRITE_HIGH();
    delay_us(DELAY_CNT);
}

(8)发送NACK信号

void i2c_send_no_ack(void)
{
    //读完数据,在第九个时钟(即第八个时钟的下降沿)发出ACK信号
    SCL_WRITE_LOW();
    delay_us(DELAY_CNT);

    SDA_WRITE_HIGH();
    delay_us(DELAY_CNT);

    //释放总线
    SCL_WRITE_HIGH();
    delay_us(DELAY_CNT);
}

在调试过程中有一个需要注意的地方:
①起始信号和停止信号的模拟,不要随便加入SCL的下降沿,因为很有可能在接收数据之后发送一个停止信号,如果此时停止信号中一开始出现SCL下降沿,会是从设备误认为还要发送数据,很有可能将SDA拉低,一旦从设备拉低了SDA,停止信号在拉高SDA时会失败,导致停止信号失效;
②在SCL高电平时,不要切换SDA的输入输出模式,因为很有可能切换SDA输入输出时,SDA引脚上会输出抖动的电平信号,而这时SCL是高电平,所以有可能会误触发起始或停止信号;
③输出要配置成开漏输出。

以上就是I2C协议软件模拟的C代码实现,下一篇将介绍软件模拟I2C的具体应用,以SHT20温湿度传感器为例。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值