跳转到内容

Khepera III 工具箱/工具箱/模块/i2cal

来自维基教科书,开放世界中的开放书籍

i2cal 模块提供了一个非常易于使用的 I2C 接口。I2C 总线是 Khepera III 机器人的“主干”,用于与三个微控制器通信

  • 主微控制器(红外传感器、超声波传感器、电池)
  • 左侧电机控制器
  • 右侧电机控制器

要使用机器人的传感器和执行器,您可能需要使用khepera3 模块,该模块包含高级函数,并使用此模块本身来与微控制器通信。但是,如果您开发了自己的扩展板,并将它们连接到机器人的顶部,此模块提供了您通过 I2C 总线访问这些板所需的一切。

// Initialize the module
i2cal_init();

// Let's assume that the I2C address of your device is 0x45
i2c_address = 0x45;

// Write the 16-bit value 10000 to register 0x82
i2cal_start();
i2cal_writedata_uint8(0x82);
i2cal_writedata_uint16(10000);
i2cal_write(i2c_address);
i2cal_commit();

// Read a 16-bit followed by two 8-bit values (4 bytes total) from register 0x83
i2cal_start();
i2cal_writedata_uint8(0x83);
i2cal_write(i2c_address);
msg_read = i2cal_read(i2c_address, 4);
i2cal_commit();

first_value = i2cal_readdata_uint16(msg_read, 0);  // read two bytes starting at byte 0
second_value = i2cal_readdata_uint8(msg_read, 2);  // read one byte starting at byte 2
third_value = i2cal_readdata_uint8(msg_read, 3);   // read one byte starting at byte 3

在初始化 (i2cal_init) 之后,i2cal 模块允许在 I2C 总线上发送事务。事务由一系列消息交换组成,要么从处理器到您的板 (写入),要么从您的板到处理器 (读取)。

事务以i2cal_start开始,并以i2cal_commit执行。在这两个调用之间,可以添加一系列读写消息。写消息通过首先写入数据 (i2cal_writedata_*) 添加,然后调用i2cal_write,并将设备地址作为参数。读消息通过i2cal_read添加,该消息将设备地址和要读取的字节数作为参数,并返回指向i2c_msg 结构的指针。

i2cal_commit 调用传输消息,并在所有数据成功发送和接收后返回。接收到的字节在i2c_msg 结构中可用,可以使用i2cal_readdata_* 函数读取。请注意,在开始新的事务之前,必须从这些结构中读取所有数据。

错误处理

[编辑 | 编辑源代码]

i2cal_commit 函数返回 -1 表示成功,或 0 表示失败。失败仅仅意味着数据无法传输,通常是由以下原因之一引起

  • 微控制器固件中的错误,即微控制器没有(或错误地)响应 I2C 请求。
  • I2C 总线上另一个微控制器的固件中的错误,导致其行为异常(例如,通过将 SDA 或 SCL 拉低)。
  • 错误的电气连接,例如 SDA/SCL 翻转,其中一个信号接地,...

有效地调试此类错误需要示波器(或类似的测试基础设施),最好是支持 I2C 解码。

如果电气设计和微控制器的固件实现正确,错误极不可能发生。因此,通常不值得实现复杂的错误处理程序。

事务大小限制

[编辑 | 编辑源代码]

i2cal 模块将事务限制为 16 条消息(读写消息组合)。此外,发送的总字节数不能超过 256,读取的总字节数也不能超过 256。鉴于下一节讨论的计时问题,这对于所有实际应用来说应该足够了。如果需要(例如,用于测试),可以在i2cal.c 中更改这些数字。

计时问题

[编辑 | 编辑源代码]

Khepera III 机器人上的 I2C 总线以 100 KHz 的速度运行。因此,通过该总线传输一个字节(8 位 + 一些开销)大约需要 0.1 毫秒。

一条消息由一个地址字节加上要读取或写入的数据组成。根据经验,事务的最小时间可以按如下方式计算

实际上,还有另外两个延迟会添加到此时间

  • 排队延迟:由于总线一次只能发送一条消息,内核会保留消息和事务的队列。您的事务将不得不与使用 I2C 总线的其他进程竞争。为了对所有进程实现低延迟,最好以小事务(一次< 100 字节)进行通信。
  • 微控制器上的处理延迟:每个传输的字节都必须由微控制器进行确认,并且每个接收到的字节都必须由微控制器准备。对于后者,微控制器允许购买大约 10 毫秒的时间(I2C 时钟拉伸),在此期间总线被占用,您的进程正在等待。为了实现良好的总线利用率,必须避免这种情况。

同步问题

[编辑 | 编辑源代码]

这里的事务之所以被称为事务,是因为它是作为一个原子块执行的。这一点非常重要,因为以下代码片段说明了这一点

// Read a 16-bit value from register 0x83 in one transaction -> correct
i2cal_start();
i2cal_writedata_uint8(0x83);
i2cal_write(i2c_address);
msg_read = i2cal_read(i2c_address, 2);
i2cal_commit();

// Read a 16-bit value from register 0x83 in two transactions -> wrong
i2cal_start();
i2cal_writedata_uint8(0x83);
i2cal_write(i2c_address);
i2cal_commit();
    // <-- Another process may communicate with the same device here, overwriting your 0x83 request
i2cal_start();
msg_read = i2cal_read(i2c_address, 2);
i2cal_commit();

如果只有一个进程在任何给定时间与该设备通信,则这两个代码片段是等效的。但是,如果多个进程正在访问该设备(并且您应该始终以这种方式思考),将任务拆分为两个事务是错误的。

要了解为什么,请将自己置于微控制器(或其他 I2C 从设备)的位置。您收到寄存器 0x83,并为接下来的读取操作准备您的输出缓冲区。由于事务在那里结束,因此另一个进程可能在第一个进程读取其两个字节之前与您通信。此进程可能发送寄存器 0x92,并且您正在准备(并且可能正在提供)您的输出缓冲区以供此新请求使用。当第一个进程最终尝试读取其两个字节时,微控制器要么提供 0x92 请求的前两个字节,要么提供一些错误代码来指示 0x92 的结果已被读取。

根据经验,每个独立的操作(例如寄存器读取或寄存器写入)必须执行为恰好一个事务。

多线程程序

[编辑 | 编辑源代码]

由于此模块的数据结构是静态分配的,因此从两个不同线程调用更新相同字段的函数是不安全的。为了避免线程之间的干扰,必须同步从**i2cal_start**到**i2cal_commit**(包含)之间的整个块。

与 Linux I2C 接口的比较

[编辑 | 编辑源代码]

Linux(以及大多数其他 Unix 系统)通过对 I2C 设备文件(/dev/i2c/0)的文件操作(特别是 ioctl 调用)提供了一个相当简单的 I2C 总线接口。如果您查看i2cal模块的源代码,您会注意到i2cal_commit只是对/dev/i2c/0的一个这样的ioctl调用,并且所有其他函数主要准备数据结构以传递给该调用。然而,i2cal模块通过处理所有必要的缓冲区,以及使用函数来正确地从这些缓冲区读取和写入 2 字节和 4 字节值,提供了更简单的接口。

以下列表总结了i2cal模块的功能。

// Module initialization
int i2cal_init();

// Start a new transaction
void i2cal_start();

// Add general read/write messages
struct i2c_msg *i2cal_read_buffer(int dev, unsigned char *buffer, int len);
struct i2c_msg *i2cal_write_buffer(int dev, unsigned char *buffer, int len);

// Simple read/write, using built-in read/write buffers (use i2cal_writedata_* functions)
struct i2c_msg *i2cal_read(int dev, int len);
struct i2c_msg *i2cal_write(int dev);

// Write data to built-in buffer
unsigned char *i2cal_writedata_uint8(unsigned char value);
unsigned char *i2cal_writedata_int16(int value);
unsigned char *i2cal_writedata_uint16(unsigned int value);
unsigned char *i2cal_writedata_int32(int value);
unsigned char *i2cal_writedata_uint32(unsigned int value);
unsigned char *i2cal_writedata_buffer(int len);

// Commit transaction by sending/receiving data. Returns the ioctl return value, which is negative on error.
int i2cal_commit();

// Read data from result buffer
unsigned char i2cal_readdata_uint8(struct i2c_msg *message, int offset);
int i2cal_readdata_int16(struct i2c_msg *message, int offset);
unsigned int i2cal_readdata_uint16(struct i2c_msg *message, int offset);
int i2cal_readdata_int32(struct i2c_msg *message, int offset);
unsigned int i2cal_readdata_uint32(struct i2c_msg *message, int offset);
华夏公益教科书