STM32F103 I2C详解

简介

IIC(Inter-Integrated Circuit)是一种串行通信的总线协议。在这条总线上可以挂载多个设备,其中控制时钟信号且可收发数据的称为主设备,只能收发数据的称为从设备。在我们学习一种通信协议的时候,需要搞清除三个问题:物理接口、时序和通信过程。下面就从这三个方面来了解IIC。

物理接口

IIC有两个接口,时钟线SCL和数据线SDA。SCL负责时钟信号,SDA则用来传输数据。

时序

在开始介绍时序前,先了解一段关于IIC的小历史。在学习IIC的时候,经常能遇到教程中使用任意的GPIO,通过软件模拟的方式来模拟IIC的时序信号,而不是使用STM32提供的硬件IIC。这是因为在过去流行使用标准库的时代,硬件IIC的库函数存在固有缺陷。这里的缺陷主要有以下几点:

  • 1.硬件I2C的库函数存在固有缺陷
    • 总线状态处理问题:标准库的硬件I2C驱动在总线异常(如干扰、从机无响应)时容易陷入“死等”状态。例如,当I2C总线处于“忙”状态时,硬件I2C可能因未正确处理超时机制而卡死,导致程序无法继续运行
    • ACK信号处理缺陷:在读取数据时,若从机未正确返回ACK信号,硬件I2C可能无法自动恢复,需手动干预总线状态(如强制拉高SDA/SCL)才能解除阻塞
  • 2.抗干扰能力不足
    • 硬件I2C对时序要求严苛:硬件I2C的时序由状态机控制,若总线受到干扰(如电压波动、信号毛刺),可能导致状态机进入异常状态,需重启外设或复位芯片才能恢复

因此,为了避免上述问题,往往使用软件模拟I2C通过GPIO手动控制时序,开发者可灵活加入超时检测和错误恢复逻辑,避免程序死锁。
如今,ST官方已经停止维护标准库,转而推广HAL库,而HAL库的硬件I2C驱动通过优化状态机设计和超时机制,上面这些问题已经得到了解决。
所以在日常开发中,建议根据使用场景来灵活的选择是使用软件模拟IIC,还是硬件IIC。
一般,在适合低速(100kHz)、高干扰的环境或需要快速移植的项目中使用软件模拟IIC,若需要高性能(400kHz)且使用HAL库,则可以使用硬件IIC。
下面在介绍时序的时候,为了更直观的看到IIC的工作过程,会给出软件模拟IIC的代码。在最后会给出HAL库下的IIC代码。

IIC总线时序图如下,总共可以分为以下几部分。
IIC

1.起始信号

当SCL为高电平时,SDA从高到低为起始信号。

void iic_start(void){
    IIC_SDA(1);
    IIC_SCL(1);
    iic_delay();
    IIC_SDA(0);     /* START信号: 当SCL为高时, SDA从高变成低, 表示起始信号 */
    iic_delay();
    IIC_SCL(0);     /* 钳住I2C总线,准备发送或接收数据 */
    iic_delay();
}
2.停止信号

当SCL为高电平时,SDA从低跳变到高为停止信号

void iic_stop(void){
    IIC_SDA(0);     /* STOP信号: 当SCL为高时, SDA从低变成高, 表示停止信号 */
    iic_delay();
    IIC_SCL(1);
    iic_delay();
    IIC_SDA(1);     /* 发送I2C总线结束信号 */
    iic_delay();
}
3.应答信号

在SCL的第九个脉冲时,SDA为低电平时,为有效应答信号ACK。SDA为高电平时,为无效应答信号NACK。

/**
 * @brief       产生ACK应答
 */
void iic_ack(void){
    IIC_SDA(0);     /* SCL 0 -> 1  时 SDA = 0,表示应答 */
    iic_delay();
    IIC_SCL(1);     /* 产生一个时钟 */
    iic_delay();
    IIC_SCL(0);
    iic_delay();
    IIC_SDA(1);     /* 主机释放SDA线 */
    iic_delay();
}
/**
 * @brief       不产生ACK应答
 */
void iic_nack(void){
    IIC_SDA(1);     /* SCL 0 -> 1  时 SDA = 1,表示不应答 */
    iic_delay();
    IIC_SCL(1);     /* 产生一个时钟 */
    iic_delay();
    IIC_SCL(0);
    iic_delay();
}
/**
 * @brief       等待应答信号到来
 * @param       无
 * @retval      1,接收应答失败
 *              0,接收应答成功
 */
uint8_t iic_wait_ack(void){
    uint8_t waittime = 0;
    uint8_t rack = 0;

    IIC_SDA(1);     /* 主机释放SDA线(此时外部器件可以拉低SDA线) */
    iic_delay();
    IIC_SCL(1);     /* SCL=1, 此时从机可以返回ACK */
    iic_delay();

    while (IIC_READ_SDA)    /* 等待应答 */
    {
        waittime++;

        if (waittime > 250)
        {
            iic_stop();
            rack = 1;
            break;
        }
    }

    IIC_SCL(0);     /* SCL=0, 结束ACK检查 */
    iic_delay();
    return rack;
}
4.数据有效性

在SCL高电平期间,SDA的电平即为发送/接收的数据。只有在SCL下降沿期间,SDA才允许变化。数据必须在SCL上升沿到来前稳定。

/**
 * @brief       IIC发送一个字节
 * @param       data: 要发送的数据
 */
void iic_send_byte(uint8_t data){
    uint8_t t;
    
    for (t = 0; t < 8; t++)
    {
        IIC_SDA((data & 0x80) >> 7);    /* 高位先发送 */
        iic_delay();
        IIC_SCL(1);
        iic_delay();
        IIC_SCL(0);
        data <<= 1;     /* 左移1位,用于下一次发送 */
    }
    IIC_SDA(1);         /* 发送完成, 主机释放SDA线 */
}
/**
 * @brief       IIC读取一个字节
 * @param       ack:  ack=1时,发送ack; ack=0时,发送nack
 * @retval      接收到的数据
 */
uint8_t iic_read_byte(uint8_t ack){
    uint8_t i, receive = 0;

    for (i = 0; i < 8; i++ )    /* 接收1个字节数据 */
    {
        receive <<= 1;  /* 高位先输出,所以先收到的数据位要左移 */
        IIC_SCL(1);
        iic_delay();

        if (IIC_READ_SDA)
        {
            receive++;
        }
        
        IIC_SCL(0);
        iic_delay();
    }

    if (!ack)
    {
        iic_nack();     /* 发送nACK */
    }
    else
    {
        iic_ack();      /* 发送ACK */
    }

    return receive;
}

通信过程

先来看看写的通信过程
IICW
如图所示,写操作的流程是主机发送起始信号S,给出从机地址(7位),数据方向(1位),从机给出应答ACK,主机发出数据DATA,从机回复应答ACK,最后主机发出停止信号。

/**
 * @brief       写入一个数据
 * @param       addr: 从机地址
 * @param       data: 要写入的数据
 */
void write_one_byte(uint16_t addr, uint8_t data){
    iic_start();                /* 发送起始信号 */
    iic_send_byte(addr); 		/* 发送地址 */
    iic_wait_ack();             /* 每次发送完一个字节,都要等待ACK */
    /* 因为写数据的时候,不需要进入接收模式了,所以这里不用重新发送起始信号了 */
    iic_send_byte(data);        /* 发送1字节 */
    iic_wait_ack();             /* 等待ACK */
    iic_stop();                 /* 产生一个停止条件 */
    delay_ms(10);               /* 注意: EEPROM 写入比较慢,必须等到10ms后再写下一个字节 */
}

读操作流程
IICR
读操作与写操作类似,区别在于从机地址后的读写位,以及数据和应答信号的发送方不一样。

/**
 * @brief       读出一个数据
 * @param     addr: 从设备地址
 * @retval      读到的数据
 */
uint8_t read_one_byte(uint16_t addr)
{
    uint8_t temp = 0;
    iic_start();                /* 发送起始信号 */
    iic_send_byte(addr);   		/* 发送低位地址据 */
    iic_wait_ack();             /* 每次发送完一个字节,都要等待ACK */ 
    temp = iic_read_byte(0);    /* 接收一个字节数据 */
    iic_stop();                 /* 产生一个停止条件 */
    return temp;
}

HAL库

与软件模拟IIC的方式比起来,HAL库的使用十分简单。以STM32F103ZET6的I2C1为演示,使用的PB6和PB7引脚。

首先初始化

I2C_HandleTypeDef hi2c = {0};	/* 句柄 */
void iic_init(void){
	
	hi2c.Instance = I2C1;
	hi2c.Init.ClockSpeed = 400000;
	hi2c.Init.DutyCycle = I2C_DUTYCYCLE_2;
	hi2c.Init.OwnAddress1 = 0;
	hi2c.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
	hi2c.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
	hi2c.Init.OwnAddress2 = 0;
	hi2c.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
	hi2c.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
	
	HAL_I2C_Init(&hi2c);
}

void HAL_I2C_MspInit(I2C_HandleTypeDef *hi2c){
	if(hi2c->Instance == I2C1){
		
		// 使能时钟
		__HAL_RCC_GPIOB_CLK_ENABLE();
		__HAL_RCC_I2C1_CLK_ENABLE();
		
		// 初始化GPIO
		GPIO_InitTypeDef gpio_init = {0};
		
		// SCL
		gpio_init.Pin = GPIO_PIN_6;
		gpio_init.Mode = GPIO_MODE_AF_OD;
		gpio_init.Speed = GPIO_SPEED_FREQ_HIGH;
		HAL_GPIO_Init(IIC_SCL_PORT, &gpio_init);
		
		// SDA
		gpio_init.Pin = GPIO_PIN_7;
		HAL_GPIO_Init(IIC_SDA_PORT, &gpio_init);
	}
}

接下来只需要调用读写函数就可以了。这里给出阻塞方式下的常用读写函数。DMA与中断方式的可以参考HAL库。

/* 主机发送数据 */
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
/* 主机读取数据 */
HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint8_t *pData, uint16_t Size, uint32_t Timeout);
/* 主机向指定地址发送数据 */
HAL_StatusTypeDef HAL_I2C_Mem_Write(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);
/* 主机从指定地址读取数据 */
HAL_StatusTypeDef HAL_I2C_Mem_Read(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress, uint16_t MemAddSize, uint8_t *pData, uint16_t Size, uint32_t Timeout);

下面给出一些使用例子

/* OLED通过iic发送数据 */
#define OLED_ADDRESS 0x78	// OLED地址
void OLED_Send(uint8_t *data, uint8_t len){
  HAL_I2C_Master_Transmit(&hi2c, OLED_ADDRESS, data, len, HAL_MAX_DELAY);
}
/*****************************************************************************/
/* AT24C02通过IIC指定地址读写 */
#define EEPROM_ADDR		0xA0	  // AT24C02地址
#define WRITE_DELAY 5             // 写入延时(单位ms)
/* 写入24C02 */
HAL_StatusTypeDef EEPROM_Write(uint16_t mem_addr, uint8_t *data, uint16_t size) {
    // 24C02的地址是8位,所以使用单字节地址模式
    HAL_StatusTypeDef status = HAL_I2C_Mem_Write(&hi2c, EEPROM_ADDR, mem_addr, I2C_MEMADD_SIZE_8BIT, data, size, HAL_MAX_DELAY);
    HAL_Delay(WRITE_DELAY);  // 等待EEPROM写入完成
    return status;
}

/* 读取24C02 */
HAL_StatusTypeDef EEPROM_Read(uint16_t mem_addr, uint8_t *data, uint16_t size) {
    return HAL_I2C_Mem_Read(&hi2c, EEPROM_ADDR, mem_addr, I2C_MEMADD_SIZE_8BIT, data, size, HAL_MAX_DELAY);
}
在 Python 中,可以使用 statsmodels 模块中的 OLS 类来进行OLS回归模型的拟合和预测。下面是一个简单的示例代码: ```python import numpy as np import pandas as pd import statsmodels.api as sm # 生成随机数据 np.random.seed(123) X = np.random.rand(100) Y = 2*X + 0.5 + np.random.normal(0, 0.1, 100) # 将数据存放在DataFrame对象中 data = pd.DataFrame({'X': X, 'Y': Y}) # 添加截距项 data = sm.add_constant(data) # 拟合OLS回归模型 model = sm.OLS(data['Y'], data[['const', 'X']]) result = model.fit() # 输出回归结果 print(result.summary()) ``` 在上面的代码中,我们首先生成了一个简单的随机数据集,其中 X 是自变量,Y 是因变量。然后,我们将数据存放在了一个 pandas 的 DataFrame 对象中,并使用 sm.add_constant() 函数添加了截距项。接着,我们使用 sm.OLS() 函数拟合了OLS回归模型,并将结果保存在了 result 变量中。最后,我们使用 result.summary() 方法输出了回归结果的详细信息。 需要注意的是,在使用 statsmodels 进行OLS回归模型拟合时,需要显式地添加截距项,否则结果会有偏差。此外,我们还可以使用 result.predict() 方法来进行预测,即: ```python # 进行预测 new_data = pd.DataFrame({'X': [0.1, 0.2, 0.3]}) new_data = sm.add_constant(new_data) prediction = result.predict(new_data) # 输出预测结果 print(prediction) ``` 在上面的代码中,我们首先生成了一个新的数据集 new_data,然后使用 result.predict() 方法对其进行预测,并将结果保存在了 prediction 变量中。最后,我们使用 print() 函数输出了预测结果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值