问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

一文搞懂I2C通信协议:原理与STM32实现详解

创作时间:
作者:
@小白创作中心

一文搞懂I2C通信协议:原理与STM32实现详解

引用
CSDN
1.
https://m.blog.csdn.net/weixin_45264425/article/details/140163843

**I2C(Inter-Integrated Circuit)是一种同步串行通信总线,由飞利浦公司在上世纪八十年代初开发。与SPI相比,I2C只需要两根信号线(SCL和SDA)就可以实现数据的双向传送,因此接线形式更简单。I2C被广泛应用于EEPROM存储设备、四轴无人机的IMU传感器(如MPU6050)等场景。本文将详细介绍I2C通信协议的工作原理,并以STM32微控制器为例,讲解其具体实现。

1. I2C通信协议基础

I2C是一种同步通信机制,两根信号线一根用于时钟信号(SCL),另一根则用于数据传送(SDA)。根据时钟信号的来源,I2C的通信设备分为主从两种模式。每次通信都是由主设备发起,并产生时钟信号用于主从设备之间的数据同步。在通信结束后,主设备需要停止产生时钟。

I2C总线上允许连接多个设备,采用寻址的方式选定从设备通信。主设备通过保持SCL为高电平信号,同时控制SDA信号产生一个下降沿,来产生一个起始信号(START)到总线上,标志着一次通信的开始。结束信号则是在保持SCL为高电平信号时,控制SDA产生一个上升沿。

I2C的通信是以字节为单位进行的,先发送高位后发送低位,每一位的数据发送都是在SCL处于高电平信号时进行的。每个字节传送完毕后,必须由数据的接收方产生一个ACK信号到总线上。如果从设备忙,或者接收方不能及时响应,它们可以把SCL信号线拉低,强制主设备进入等待状态。

2. STM32的I2C外设模块

STM32的I2C外设模块结构如图所示,对外有三个引脚SCL、SDA和SMBA。有两个控制寄存器CR1和CR2用于控制逻辑,通过它们可以触发起始和停止信号,做出ACK响应,配置外设时钟频率,开启DMA和中断的功能。同时控制逻辑的状态会反馈到SR1和SR2两个状态寄存器上。

STM32专门提供了一个时钟控制器用于驱动同步时钟信号线SCL。通过配置CCR寄存器,可以调整SCL的频率。还有一个与SCL有关的寄存器TRISE,它定义了通信过程中时钟信号上升沿的最大时间。

数据的收发主要涉及到数据寄存器(DR)和数据移位寄存器(DSR)。当需要发送数据时,把要发送的字节写入DR寄存器。硬件会判定DSR寄存器是否为空,把DR中的字节搬到DSR中。然后在时钟信号的控制下,把DSR最高位的数据放到数据线SDA上,并对DSR进行移位操作。当8位数据通过移位操作发送完毕之后,如果通信没有结束,将再次从DR中搬一个字节到DSR中,继续发送数据。

当设备工作在接收机状态下时,数据控制器根据时钟信号,把SDA线上的高低电平转换为‘1’‘0’数据,写到DSR的最低位。同时DSR左移位,当接收完一个字节的8位数据后,把DSR中的数据搬到DR寄存器中。再从DR寄存器中把数据读出来,模块会产生一个ACK信号到总线上。

在STM32中,I2C模块可以工作主从两种模式下。模块有两个地址寄存器,通过与DSR寄存器中接收的数据对比,来监听总线上的地址信号。如果能够匹配上,则建立起一个连接与主设备进行通信。

3. I2C通信的实现

3.1 初始化过程

在使用I2C外设之前,需要先对它进行初始化操作。主要涉及三个方面的工作:

  • 打开外设相关的时钟
  • 对外设涉及的引脚进行配置
  • 修改外设的寄存器配置外设的工作方式

整个过程如函数i2c1_init()的实现:

void i2c1_init(void) {
    // 开启外设时钟
    RCC->AHB1ENR.bits.gpiob = 1;
    RCC->APB1ENR.bits.i2c1 = 1;
    // 配置引脚功能
    GPIOB->AFR.bits.pin8 = GPIO_AF_I2C1;
    GPIOB->AFR.bits.pin9 = GPIO_AF_I2C1;
    struct gpio_pin_conf pincof;
    pincof.mode = GPIO_Mode_Af;
    pincof.otype = GPIO_OType_OD;
    pincof.pull = GPIO_Pull_Up;
    pincof.speed = GPIO_OSpeed_High;
    gpio_init(GPIOB, GPIO_Pin_8 | GPIO_Pin_9, &pincof);
    // 配置外设
    i2c_init(I2C1);
}

在函数i2c_init()中对外设I2C1进行初始化配置。首先在3~6行重置外设,使得外设的各种控制器恢复到初始状态,放置异常的状态导致总线死锁。接着在第8行,设置CR2寄存器设定I2C的工作频率为42MHz。

然后在10~13行设置TRISE和CCR寄存器,配置同步时钟信号线的频率和上升沿时间,因为对这两寄存器的改写必须保证外设关闭,所以在10行中先关闭外设,再在第13行中打开。我们配置CR1寄存器,使得外设在接收到数据后能够返回一个ACK信号。

最后在第16行,我们随意指定了本地的地址为0xC0。

关于初始化过程,需要注意的是,第12行对CCR的配置。在STM32中,I2C有标准和快速两种工作方式,标准模式总线上的时钟频率小于100KHz,快速的为400KHz,这点通过配置CCR寄存器来实现。CCR寄存器中有三个字段F/S、DUTY和CCR。F/S设定了外设的工作模式,对应CCR寄存器的第16位,为1表示快速模式。DUTY则定义了时钟信号的占空比,这里的配置是低电平的时间时高电平时间的两倍。CCR则定义了SCL的频率。

3.2 读写操作

下面的函数i2c_read_bytes()实现了从外设中读取一段数据的操作。它由5个参数,i2c是一个指向外设寄存器的指针,可以的取值为I2C1,I2C2和I2C3。addr则是目标从设备的地址。reg描述了访问外设的寄存器地址,len则描述了欲读取的字节数,buf则指向了一篇缓存用于存放读取的数据。

函数的工作流程在程序中的注释已经写得比较清楚了:

void i2c_read_bytes(i2c_regs_t *i2c, uint8 addr, uint8 reg, uint8 len, uint8 *buf) {
    // 总线空闲
    while (1 == i2c->SR2.bits.BUSY);
    // 产生START,进入Master模式
    i2c->CR1.bits.START = 1;
    while (TRUE != i2c_check_status(i2c, I2C_STA_BUSY_MSL_SB));
    // 发送7位地址, 发送模式
    i2c->DR = (addr << 1) | I2C_DIRECTION_TX;
    while (TRUE != i2c_check_status(i2c, I2C_STA_BUSY_MSL_ADDR_TXE_TRA));
    // 发送寄存器地址
    i2c->DR = reg;
    while (TRUE != i2c_check_status(i2c, I2C_STA_BUSY_MSL_TXE_TRA_BTF));
    // 重新产生起始位, 进入Master Receiver模式
    i2c->CR1.bits.START = 1;
    while (TRUE != i2c_check_status(i2c, I2C_STA_BUSY_MSL_SB));
    i2c->DR = (addr << 1) | I2C_DIRECTION_RX;
    while (TRUE != i2c_check_status(i2c, I2C_STA_BUSY_MSL_ADDR));
    // 依次接收字节,每次接收都需要返回一个ACK
    i2c->CR1.bits.ACK = 1;
    while (len > 1) {
        while (TRUE != i2c_check_status(i2c, I2C_STA_BUSY_MSL_RXNE));
        buf[0] = i2c->DR;
        buf++; len--;
    }
    // 读最后一个字节,所以返回NACK
    i2c->CR1.bits.ACK = 0;
    i2c->CR1.bits.STOP = 1;
    while (TRUE != i2c_check_status(i2c, I2C_STA_BUSY_MSL_RXNE));
    buf[0] = i2c->DR;

    i2c->CR1.bits.ACK = 1;
}

类似的,我们定义了函数i2c_write_bytes()用于向外设发送一堆数据。它也有5个参数,定义与读操作的参数一样,只是最后一个参数buf指引的是要发送数据的缓存。因为不希望在函数中对原来的数据做出修改,所以加上了const修饰符。

void i2c_write_bytes(i2c_regs_t *i2c, uint8 addr, uint8 reg, uint8 len, const uint8 *buf) {
    // 总线空闲
    while (1 == i2c->SR2.bits.BUSY);
    // 产生START,进入Master模式
    i2c->CR1.bits.START = 1;
    while (TRUE != i2c_check_status(i2c, I2C_STA_BUSY_MSL_SB));
    // 发送7位地址, 发送模式
    i2c->DR = (addr << 1) | I2C_DIRECTION_TX;
    while (TRUE != i2c_check_status(i2c, I2C_STA_BUSY_MSL_ADDR_TXE_TRA));
    // 发送寄存器地址
    i2c->DR = reg;
    while (TRUE != i2c_check_status(i2c, I2C_STA_BUSY_MSL_TXE_TRA_BTF));
    // 发送数据
    while (len--) {
        i2c->DR = buf[0];
        while (TRUE != i2c_check_status(i2c, I2C_STA_BUSY_MSL_TXE_TRA_BTF));
        buf++;
    }

    i2c->CR1.bits.STOP = 1;
}

另外,为了使用方便,我们还定义了i2c_read_byte()i2c_write_byte()两个函数用于读取和发送一个字节的操作。这两个函数都只是对上述两个函数的一个封装而已。

uint8 i2c_read_byte(i2c_regs_t *i2c, uint8 addr, uint8 reg) {
    uint8 data;
    i2c_read_bytes(i2c, addr, reg, 1, &data);
    return data;
}
void i2c_write_byte(i2c_regs_t *i2c, uint8 addr, uint8 reg, uint8 data) {
    i2c_write_bytes(i2c, addr, reg, 1, &data);
}

3.3 硬件I2C驱动MPU6050

MPU6050是一个六轴的IMU传感器,集成了陀螺仪和加速度计,在无人机等智能设备中广泛的应用。这里简单的介绍通过I2C从MPU6050中读取数据,详细的传感器使用参见一个四旋翼的飞控系统系列文章。

STM32F407的引脚PB8和PB9分别对应了I2C1的SCL和SDA,在探索者的开发板上它们挂载了一个MPU6050和一个24C02的EEPROM。在main函数中,我们先调用i2c1_init()对I2C1的外设进行初始化,然后就可以通过函数i2c_read_byte()从传感器中读取数据。这里访问的传感器的0x75的寄存器,它描述了传感器的设备ID。

uint8 gtestdata;
int main(void) {
    usart1_init(115200);
    i2c1_init();

    config_interruts();

    Delay(168000000);

    uint8 id = i2c_read_byte(I2C1, 0x68, 0x75);
    usart1_send_bytes((uint8*)&id, 1);
    while (1) { }
}

我们可以看到在系统复位一段时间后,就可以通过串口发送一个值为0x68的字节,它正是mpu6050的I2C地址。详细参见源码。

4. 总结

STM32的I2C可以工作在主从两种模式下,具体由发送起始信号还是接收到匹配地址来决定。虽然人们一直在说STM32的I2C实现有些问题,但经过测试发现,意法半导体已经解决了这个问题,硬件实现也没有什么毛病。

I2C通信协议以其简单易用、接线少等特点,在嵌入式系统中得到了广泛应用。通过本文的介绍,相信读者已经对I2C通信协议有了全面的了解,并能够基于STM32微控制器实现具体的I2C通信应用。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号