Skip to content

stm32软件IIC

约 1941 字大约 6 分钟

2025-08-05

前言

IIC(Inter-Integrated Circuit)是 IIC Bus 简称,一主多从半双工总线型通讯协议。它是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代为了让主板、嵌入式系统或手机用以连接低速周边设备而发展。自2006年10月1日起,使用I²C协议已经不需要支付专利费,但制造商仍然需要付费以获取I²C从属设备地址。本文主要介绍软件IIC的实现方法。

一、主要特性

  • IIC通过SDASCL两根信号线进行通信。

  • IIC通信协议支持多主多从架构,每个设备都有一个唯一的地址,主设备和从设备之间通过地址来进行通信。

  • IIC通信协议支持数据的双向传输,主设备可以向从设备发送数据,从设备也可以向主设备发送数据。

  • SDA和SCL都需要配置为上拉模式开漏输出。(如果使用推挽输出,当两个设备同时发送数据时,会导致信号冲突,严重会导致芯片短路)

  • IIC总线所有器件都具有自动应答功能,无论是发送和接收都会发送一个应答ACK或NACK信号。


需要全面了解IIC协议,可以查看官方文档

image-20250805213948270
image-20250805213948270

二、工作时序

2.1 启动信号和停止信号

在空闲情况下,SDA和SCL都是高电平: image-20250805214752951

IIC延时函数
/*******************************************************************************
 * @brief       IIC延时函数
 * @param
 * @retval
 */
static void iic_delay(void)
{
    delay_us(2); /* 延时2us */
}

在开始正式代码前,需要先注意一下IIC延时函数,因为每台IIC协议的解析速率不一,所以为了保证通讯的稳定性,需要加入延时函数,延时的时间根据需要通讯的设备来确定。

频率延时时间
100kHz1us
400kHz0.25us
1MHz0.1us
200kHz0.05us

常见频率

2.1.1 启动信号

启动信号是由主设备发送给从设备的信号,用于通知从设备开始通信。启动信号的时序图如下:

IIC启动信号时序
IIC启动信号时序

由上图我们可以看到IIC的启动时序,我们也由此开始编写代码,通过代码可以看出,首先确定SDA和SCL都已经释放(已经拉高),然后delay防止主机速度过快,接着看是发送我们的起始信号,从图中我们看出,先将SDA从拉到,经过一定的时间周期以后,再将SCL拉低,由此IIC的起始信号就完成了。

IIC启动函数
/*******************************************************************************

 * @brief       IIC Start函数
 * @param
 * @retval
 */
void iic_start(void)
{
    SDA(1);
    SCL(1);
    iic_delay();
    SDA(0);
    iic_delay();
    SCL(0);
}

2.1.2 停止信号

停止信号是由主设备发送给从设备的信号,用于通知从设备结束通信。停止信号的时序图如下:

IIC停止信号时序图
IIC停止信号时序图

通过代码可以看到,首先将SDA拉低,然后将SCL拉高,最后将SDA拉高,这样就完成了IIC的停止信号。

IIC停止函数
/*******************************************************************************
 * @brief       IIC stop函数
 * @param
 * @retval
 */
void iic_stop(void)
{
    SDA(0);
    SCL(1);
    iic_delay();
    SDA(1);
}

2.2 应答信号

应答信号时序图
应答信号时序图

由于IIC在接收数据和发送数据都需要发送应答,所以我们在封装函数的时候需要考虑到这一点。

发送应答
/*******************************************************************************
 * @brief       IIC 发送ACK
 * @param       ack: 0-发送ACK, 1-发送NACK
 * @retval
 */
void iic_send_ack(uint8_t ack)
{
    if (ack)
    {
        SDA(1); /* 发送NACK */
    }
    else
    {
        SDA(0); /* 发送ACK */
    }
    SCL(1);
    iic_delay();
    SCL(0);
}

2.3 数据传输

image-20250806013036699
image-20250806013036699
发送1bit
/*******************************************************************************


 * @brief       IIC发送一字节
 * @param
 * @retval
 */
void iic_send_byte(uint8_t byte)
{
    uint8_t i;

    for (i = 0; i < 8; i++)
    {
        SDA(byte & (0x80 >> i)); /* 先发送最高位 */
        iic_delay();
        SCL(1);
        iic_delay();
        SCL(0);
        iic_delay();
    }
    SDA(1); /* 主机释放SDA */
    iic_delay();
}

三、完整代码

在移植完整代码时,对于stm平台,只需修改IIC.h中的引脚定义即可。如果是其他平台,还需根据平台的GPIO库函数修改iic_init(void)函数。

IIC.c
#include "./24C02_IIC.h"

/**************************************************************************************************
 * @brief       IIC初始化函数
 * @param
 * @retval
 */
void iic_init(void)
{
    /* 使能SCL和SDA的时钟 */
    SCL_GPIO_CLK_ENABLE();
    SDA_GPIO_CLK_ENABLE();

    GPIO_InitTypeDef gpio_init_strcut;
    gpio_init_strcut.Mode = GPIO_MODE_OUTPUT_OD;
    gpio_init_strcut.Pin = SCL_GPIO_PIN;
    gpio_init_strcut.Pull = GPIO_PULLUP;
    gpio_init_strcut.Speed = GPIO_SPEED_HIGH;
    HAL_GPIO_Init(SCL_GPIO_PORT, &gpio_init_strcut);

    gpio_init_strcut.Pin = SDA_GPIO_PIN;
    HAL_GPIO_Init(SDA_GPIO_PORT, &gpio_init_strcut);

    SDA(1);
    SCL(1);
}

/**************************************************************************************************
 * @brief       IIC延时函数
 * @param
 * @retval
 */
static void iic_delay(void)
{
    delay_us(2); /* 延时2us */
}

/**************************************************************************************************
 * @brief       IIC Start函数
 * @param
 * @retval
 */
void iic_start(void)
{
    SDA(1);
    SCL(1);
    iic_delay();
    SDA(0);
    iic_delay();
    SCL(0);
}

/**************************************************************************************************
 * @brief       IIC stop函数
 * @param
 * @retval
 */
void iic_stop(void)
{
    SDA(0);
    SCL(1);
    iic_delay();
    SDA(1);
}

/**************************************************************************************************
 * @brief       IIC 发送ACK
 * @param       ack: 0-发送ACK, 1-发送NACK
 * @retval
 */
void iic_send_ack(uint8_t ack)
{
    if (ack)
    {
        SDA(1); /* 发送NACK */
    }
    else
    {
        SDA(0); /* 发送ACK */
    }
    SCL(1);
    iic_delay();
    SCL(0);
}

/**************************************************************************************************
 * @brief       IIC 接收ACK
 * @param
 * @retval
 */
uint8_t iic_recv_ack(void)
{
    uint8_t ack = 0;
    uint8_t waittime = 0;

    SDA(1); /* 主机释放SDA */
    iic_delay();
    SCL(1); /* 拉高SCL */
    iic_delay();
    while(SDA_READ()) 
    {
        waittime++;
        if(waittime > 250) /* 等待时间超过250us,则认为没有应答 */
        {
            iic_stop(); /* 停止IIC */
            ack = 1; /* 返回NACK */
            break;
        }
    }
    SCL(0); /* 拉低SCL */
    iic_delay();

    return ack; /* 返回ACK状态 */
}

/**************************************************************************************************
 * @brief       IIC发送一字节
 * @param
 * @retval
 */
void iic_send_byte(uint8_t byte)
{
    uint8_t i;

    for (i = 0; i < 8; i++)
    {
        SDA(byte & (0x80 >> i)); /* 先发送最高位 */
        iic_delay();
        SCL(1);
        iic_delay();
        SCL(0);
        iic_delay();
    }
    SDA(1); /* 主机释放SDA */
    iic_delay();
}

/**************************************************************************************************
 * @brief       IIC接收一字节
 * @param
 * @retval
 */
uint8_t iic_recv_byte(uint8_t ack)
{
    uint8_t recv = 0;
    SDA(1); /* 主机释放SDA */
    iic_delay();
    
    SCL(1); /* 拉高SCL */
    iic_delay();

    for (uint8_t i = 0; i < 8; i++)
    {
        SCL(1); /* 拉高SCL */
        iic_delay();
        recv <<= 1; /* 左移一位 */
        if (SDA_READ()) /* 读取SDA */
        {
            recv |= 0x01; /* 如果SDA为1,则设置最低位为1 */
        }
        SCL(0); /* 拉低SCL */
        iic_delay();
    }

    iic_send_ack(ack); /* 发送ACK或NACK */
    return recv;
}