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实现,同时在主程序中只需关注接口调用,实现了面向对象的设计理念。