一盏黄黄旧旧的灯

一盏黄黄旧旧的灯

I²C总线

I²C(Inter-Integrated Circuit)总线是一种常用的双向、同步、二线制串行总线,用于短距离、低速率设备间的通信。它由两根信号线组成:

串行数据线(SDA):用于传输数据,通常采用开漏或集电极开路输出,并通过上拉电阻连接到电源电压(通常是3.3V或5V)。数据以位的形式在SDA线上发送和接收。

串行时钟线(SCL):由主机控制,用于同步数据传输的时序。所有连接到I²C总线的设备都遵循相同的时钟信号。

基本操作:

起始条件:当SCL线保持高电平时,SDA线由高电平向低电平的跳变表示起始条件,标志着一次通信的开始。
停止条件:在SCL线保持高电平时,SDA线由低电平向高电平的跳变表示停止条件,标志着一次通信的结束。
数据传输:数据在每个SCL时钟周期的上升沿被采样,在下降沿改变。每个字节后跟一个应答位(ACK),由接收方在第9个时钟周期拉低SDA线表示已正确接收数据。
寻址:起始条件后,主机先发送7位设备地址(包括设备类型和设备地址)和一位读写位(R/W),决定是向设备写数据还是从设备读数据。

SDA SCL 描述
H H 静态高电平,等待起始条件
---------------------- 起始条件
L H SDA由高变低,同时SCL保持高电平

D7 D6 D5 D4 D3 D2 D1 D0 SDA 逐位发送地址(高位到低位),SCL逐次变低和变高
A H 接收设备在第9个时钟周期拉低SDA,发送地址应答

D7 D6 D5 D4 D3 D2 D1 D0 SDA逐位发送数据字节(高位到低位),SCL逐次变低和变高
A H 接收设备在每个数据字节后第9个时钟周期拉低SDA,发送数据应答
---------------------- (重复数据传输,根据实际通信需求)
---------------------- 停止条件
H H SDA由低变高,同时SCL保持高电平
H H 静态高电平,通信结束
说明:

H 表示高电平。
L 表示低电平。
Dn 表示地址或数据的第n位(n=7…0,从高位到低位)。
A 表示应答位(ACK),即接收设备在第9个时钟周期拉低SDA表示已接收并确认该字节。
这是一个简化的示例,实际通信可能涉及多个数据字节的读写,以及可能出现的重试、无应答等情况。

实现

面向对象方法实现C语言例子:
为了使用面向对象方法实现I²C总线操作,我们可以创建一个I²C接口类,定义公共操作,如发送起始条件、停止条件、数据等,并为特定硬件平台(如GPIO模拟或硬件I²C外设)提供不同的实现。这里展示一个基于接口和实现分离的设计,使用C语言结构体和函数指针来模拟面向对象的特性。
以下是完整的面向对象方法实现I²C总线操作的C语言例子,包括接口定义、GPIO模拟I²C实现、通用I²C操作函数以及主程序:

// I2CInterface.h
#ifndef I2C_INTERFACE_H
#define I2C_INTERFACE_H

#include <stdint.h>

typedef struct {
    void* context;  // 上下文指针,用于传递给实现函数

    void (*send_start)(void*);
    void (*send_stop)(void*);
    void (*send_byte)(void*, uint8_t);
    uint8_t (*receive_byte)(void*, bool);
} I2CInterface;

// 初始化I²C接口
void i2c_interface_init(I2CInterface* interface,
                         void (*send_start_impl)(void*),
                         void (*send_stop_impl)(void*),
                         void (*send_byte_impl)(void*, uint8_t),
                         uint8_t (*receive_byte_impl)(void*, bool));

// 通用I²C操作函数(基于接口)
void i2c_write(I2CInterface* interface, uint8_t slave_addr, uint8_t reg_addr, uint8_t data);
uint8_t i2c_read(I2CInterface* interface, uint8_t slave_addr, uint8_t reg_addr);

#endif /* I2C_INTERFACE_H */

GPIO模拟I²C实现:

// GPIOI2CImplementation.h
#ifndef GPIO_I2C_IMPLEMENTATION_H
#define GPIO_I2C_IMPLEMENTATION_H

#include "I2CInterface.h"

typedef struct {
    // ... GPIO相关成员变量(如引脚编号、状态等)
} GPIOI2CImplementation;

// 初始化GPIO模拟I²C实现
GPIOI2CImplementation* gpio_i2c_impl_init(uint8_t sda_pin, uint8_t scl_pin);

// GPIO模拟I²C实现的接口实现函数
void gpio_i2c_send_start(GPIOI2CImplementation* impl);
void gpio_i2c_send_stop(GPIOI2CImplementation* impl);
void gpio_i2c_send_byte(GPIOI2CImplementation* impl, uint8_t byte);
uint8_t gpio_i2c_receive_byte(GPIOI2CImplementation* impl, bool send_ack);

// 释放GPIO模拟I²C实现资源
void gpio_i2c_impl_cleanup(GPIOI2CImplementation* impl);

#endif /* GPIO_I2C_IMPLEMENTATION_H */

// GPIOI2CImplementation.c
#include "GPIOI2CImplementation.h"

// ... 实现GPIO模拟I²C的相关函数

#endif /* GPIO_I2C_IMPLEMENTATION_C */

通用I²C操作函数:

// I2CInterface.c
#include "I2CInterface.h"

// 向指定地址的从设备写一个字节数据
void i2c_write(I2CInterface* interface, uint8_t slave_addr, uint8_t reg_addr, uint8_t data) {
    interface->send_start(interface->context);  // 发送起始条件

    // 发送设备地址和写操作标志
    interface->send_byte(interface->context, slave_addr << 1 | I2C_WRITE);

    // 确保收到从设备的ACK
    if (interface->receive_byte(interface->context, false)) {
        printf("Slave did not ACK address\n");
        return;  // 或者抛出错误,根据实际需求处理
    }

    // 发送寄存器地址
    interface->send_byte(interface->context, reg_addr);

    // 确保收到从设备的ACK
    if (interface->receive_byte(interface->context, false)) {
        printf("Slave did not ACK register address\n");
        return;  // 或者抛出错误,根据实际需求处理
    }

    // 发送数据
    interface->send_byte(interface->context, data);

    // 确保收到从设备的ACK
    if (interface->receive_byte(interface->context, false)) {
        printf("Slave did not ACK data\n");
        return;  // 或者抛出错误,根据实际需求处理
    }

    interface->send_stop(interface->context);  // 发送停止条件
}

// 从指定地址的从设备读取一个字节数据
uint8_t i2c_read(I2CInterface* interface, uint8_t slave_addr, uint8_t reg_addr) {
    interface->send_start(interface->context);  // 发送起始条件

    // 发送设备地址和写操作标志
    interface->send_byte(interface->context, slave_addr << 1 | I2C_WRITE);

    // 确保收到从设备的ACK
    if (interface->receive_byte(interface->context, false)) {
        printf("Slave did not ACK address\n");
        return 0xFF;  // 或者抛出错误,根据实际需求处理
    }

    // 发送寄存器地址
    interface->send_byte(interface->context, reg_addr);

    // 确保收到从设备的ACK
    if (interface->receive_byte(interface->context, false)) {
        printf("Slave did not ACK register address\n");
        return 0xFF;  // 或者抛出错误,根据实际需求处理
    }

    interface->send_start(interface->context);  // 发送重复起始条件(重新寻址,切换到读模式)

    // 发送设备地址和读操作标志
    interface->send_byte(interface->context, slave_addr << 1 | I2C_READ);

    // 确保收到从设备的ACK
    if (interface->receive_byte(interface->context, false)) {
        printf("Slave did not ACK read request\n");
        return 0xFF;  // 或者抛出错误,根据实际需求处理
    }

    // 读取数据并发送ACK
    uint8_t data = interface->receive_byte(interface->context, true);

    interface->send_stop(interface->context);  // 发送停止条件

    return data;
}

主程序:

// main.c
#include "I2CInterface.h"
#include "GPIOI2CImplementation.h"

int main() {
    GPIOI2CImplementation* gpio_i2c_impl = gpio_i2c_impl_init(2, 3);  // 初始化GPIO模拟I²C实现

    I2CInterface i2c_interface;
    i2c_interface_init(&i2c_interface,
                       (void (*)(void*)) gpio_i2c_send_start,
                       (void (*)(void*)) gpio_i2c_send_stop,
                       (void (*)(void*, uint8_t)) gpio_i2c_send_byte,
                       (uint8_t (*)(void*, bool)) gpio_i2c_receive_byte);

    // 设置上下文为GPIO模拟I²C实现的指针
    i2c_interface.context = gpio_i2c_impl;

    // 通过I²C接口进行通信
    i2c_write(&i2c_interface, 0x48, 0x01, 0x55);  // 向从设备0x48的寄存器0x01写入数据0x55
    uint8_t read_data = i2c_read(&i2c_interface, 0x48, 0x02);  // 从从设备0x48的寄存器0x02读取数据
    printf("Read data from slave: 0x%02X\n", read_data);

    gpio_i2c_impl_cleanup(gpio_i2c_impl);  // 释放GPIO模拟I²C实现资源

    return 0;
}

小结

这个例子中,I2CInterface结构体扮演了接口的角色,定义了与I²C通信相关的公共操作。GPIOI2CImplementation结构体则是针对GPIO模拟I²C的一种具体实现,包含了实现这些操作所需的特定代码和状态信息。通过将实现函数指针赋值给接口结构体,可以灵活地使用不同的I²C实现,同时在主程序中只需关注接口调用,实现了面向对象的设计理念。

04-20 20:39