要学习如何在 nRF Connect SDK 中设置 I2C,我们将重点关注 I2C 控制器 API。
1. 通过在应用程序配置文件 prj.conf 中添加以下行来启用 I2C 驱动程序。
CONFIG_I2C=yKconfig2. 在源代码文件中包含 I2C API 的头文件。
#include <zephyr/drivers/i2c.h>C与第 2 课中介绍的 GPIO 驱动程序一样,Zephyr 中的通用 I2C 驱动程序具有一个特定于 API 的结构体 i2c_dt_spec,其签名如下:

该结构体包含 I2C 总线 const struct device *bus 的设备指针和目标地址 uint16_t addr。
要检索此结构体,我们需要使用特定于 API 的函数 I2C_DT_SPEC_GET(),其签名如下:

3. 指定您的设备(传感器)连接到哪个 I2C 控制器及其 I2C 地址。
如果板卡的设备树中尚未定义传感器,则需要使用设备树覆盖文件手动将传感器作为子设备树节点添加到 I2C 控制器中。
3.1 创建覆盖文件。
正如我们在第 3 课中所学,从详情视图中展开 Config files->Devicetree 并创建一个覆盖文件。

这将在您的应用程序根目录中创建一个空的覆盖文件。
您也可以手动创建覆盖文件。文件必须以 .overlay 扩展名结尾,并且名称应与设备的板卡目标匹配。建议将覆盖文件存储在应用程序文件夹中名为“boards”的专用子文件夹中。但是,将它们保存在应用程序的根目录中也是可以接受的。
例如,如果您的板卡目标是 nrf52840dk/nrf52840,则相应的覆盖文件应命名为 nrf52840dk_nrf52840.overlay。基本上是将 / 替换为 _ 。
3.2 在覆盖文件中,指定您的传感器连接到的 I2C 控制器及其地址。
根据所使用的 Nordic 芯片,可能会有多个 I2C 控制器,因此请务必选择连接到传感器的控制器。
这可以通过检查板卡或开发套件的原理图来确认,以找到连接到 SDA 和 SCL 的引脚。我们将在本课的练习部分对此进行检查。您可以使用 VS Code 中的 DeviceTree 查看器来显示可用 I2C 控制器的设备树节点,它会告诉您控制器使用了哪些引脚。
在下面的插图中,我们假设传感器连接到 i2c0 控制器,并且它具有目标地址 0x4a(来自传感器数据表),我们将其标记为 mysensor。

您至少需要指定 compatible 属性、I2C 目标设备的地址及其标签。
&i2c0 {
mysensor: mysensor@4a{
compatible = "i2c-device";
reg = < 0x4a >;
label = "MYSENSOR";
};
};Devicetree注意,对于 compatible 成员,如果 SDK 中存在该传感器的驱动程序,您可以指定它。但是,本课的重点是原始 I2C 事务。
4. 定义节点标识符
下面这行代码使用设备树宏 DT_NODELABEL() 来获取节点标识符符号 I2C0_NODE,它将代表 I2C 硬件控制器 i2c0。
#define I2C0_NODE DT_NODELABEL(mysensor)CI2C0_NODE 包含有关用于 SDA 和 SCL 的引脚、I2C 控制器的内存映射、默认 I2C 频率以及目标设备地址的信息。
5. 检索特定于 API 的设备结构体。
宏调用 I2C_DT_SPEC_GET() 返回结构体 i2c_dt_spec,其中包含 I2C 总线的设备指针以及目标地址。
static const struct i2c_dt_spec dev_i2c = I2C_DT_SPEC_GET(I2C0_NODE);C6. 使用 device_is_ready() 验证设备是否已准备就绪。
if (!device_is_ready(dev_i2c.bus)) {
printk("I2C bus %s is not ready!\n\r",dev_i2c.bus->name);
return;
}C我们现在拥有一个设备结构体 i2c_dt_spec *dev_i2c,可以将其传递给 I2C 通用 API 接口以执行读/写操作。
向目标设备写入数据的最简单方法是通过函数 i2c_write_dt(),其签名如下:

例如,以下代码片段 向 I2C 目标设备同步写入 2 个字节。
具体来说,我们将值 0x8C 写入 I2C 设备上的内部寄存器 0x03。
uint8_t config[2] = {0x03,0x8C};
ret = i2c_write_dt(&dev_i2c, config, sizeof(config));
if(ret != 0){
printk("Failed to write to I2C device address %x at reg. %x \n\r", dev_i2c.addr,config[0]);
}Ci2c_read_dt(),其签名如下:
此函数要求您首先对要读取的内部寄存器调用 i2c_write_dt()。
例如,以下代码片段从 I2C 目标设备的内部寄存器 0x03 同步读取 1 个字节,假设我们在调用此代码片段之前调用了之前的写入代码片段。
uint8_t data;
ret = i2c_read_dt(&dev_i2c, &data, sizeof(data));
if(ret != 0){
printk("Failed to read from I2C device address %x at Reg. %x \n\r", dev_i2c.addr,config[0]);
}C或者,您可以使用 i2c_reg_read_byte_dt(),它将寄存器地址写入和数据读取合并为一个函数调用。此函数非常适合从设备直接读取单个字节。

从 I2C 设备读取数据的另一种方法是使用 i2c_burst_read_dt() 函数。此函数按顺序从多个寄存器读取数据。其签名如下:

例如,以下代码片段从地址“BH1749_RED_DATA_LSB”指向的寄存器的第 1 个字节开始,读取 3 个寄存器/6 个字节的数据。
uint8_t rgb_value[6]= {0};
//Do a burst read of 6 bytes as each color channel is 2 bytes
ret = i2c_burst_read_dt(&dev_i2c, BH1749_RED_DATA_LSB,rgb_value,sizeof(rgb_value));C本课的练习 2 中将更详细地使用和解释此函数。
对于 I2C 设备,通常使用函数 i2c_write_read_dt() 连续执行写入和读取数据操作,其签名如下:

这种情况的一个常见场景是先写入要读取的内部寄存器的地址,然后紧接着进行读取以获取该寄存器的内容。我们将在本课的练习部分更详细地演示这一点。
例如,以下代码片段将值 sensor_regs[0] = 0x02 写入地址为 0x4A 的 I2C 设备,然后从该设备读取 1 个字节并将其保存在变量 temp_reading[0] 中。
uint8_t sensor_regs[2] ={0x02,0x00};
uint8_t temp_reading[2]= {0};
int ret = i2c_write_read_dt(&dev_i2c,&sensor_regs[0],1,&temp_reading[0],1);
if(ret != 0){
printk("Failed to write/read I2C device address %x at Reg. %x \n\r", dev_i2c.addr,sensor_regs[0]);
}C