在本练习中,我们将使用搭载在 Waveshare 15231 分线板上的 BME280 传感器来提供温度读数。我们选择了一款支持 I2C 和 SPI 通信协议的分线板,以便在多个课程中使用该传感器。下图显示了 BME280 模块和分线板。

Waveshare 15231 分线板
Thingy 不支持本练习。如果您使用的是 Thingy 原型设计平台(Thingy:53、Thingy:91、Thingy:91 X),请参阅本课的练习 2。
Waveshare 15231 板可以通过现有的排针连接器轻松连接到我们的任何开发套件。传感器板应使用 P1 连接器连接到 DK 板(本例中为 nRF54L15)。引脚 P1.11 将配置为 SCL,P1.12 配置为 SDA。VDDIO 和 GND 应作为 VCC 和 GND 连接到传感器板。

BME280 传感器提供三种传感器模式:睡眠模式、强制模式和正常模式。传感器上电后,默认进入睡眠模式。我们应该将传感器配置为上电后进入正常或强制模式。也就是说,我们需要在读取数值之前配置传感器的模式位。
与 BME280 的所有通信都是通过读取和写入 8 位宽的寄存器来完成的。BME280 的内存映射如下所示:

BME280 寄存器内存映射
某些寄存器是保留的,不应更改。校准数据是只读的,因为这些值在生产时已经固定。控制寄存器具有读写能力,用于控制设置。数据寄存器是传感器执行感测操作后返回或存储的数值,因此是只读的。状态和芯片 ID 也是只读的,而复位是只写的。
数据寄存器提供 20 位压力值、20 位温度值和 16 位湿度值。由于所有寄存器都是 8 位宽,我们将执行多字节读取操作,然后通过将位/字节放在适当的位置来构建数值。要在转换后读取数据值,建议使用突发模式(burst mode),而不是单独寻址每个寄存器。数据读取通过从 0xF7 到 0xFC(针对温度和压力)或从 0xF7 到 0xFE(针对温度、压力和湿度)启动突发读取来完成。
温度和压力数据以无符号 20 位格式读取,湿度以无符号 16 位格式读取。这些读取值代表未补偿测量值。实际的温度、压力和湿度值随后使用补偿参数计算得出。这些补偿参数在生产时存储在设备的非易失性存储器中。下表显示了这些补偿参数的寄存器地址、内容和数据类型。

BME280 补偿参数寄存器地址
1. 在本课程的 GitHub 仓库中,打开位于 l6/l6_e1 的本练习基础代码。请注意,BME280 传感器用于 v2.7.0 及以上版本。
2. 让我们通过在应用程序配置文件 prj.conf 中添加以下行来启用 I2C 驱动程序。
CONFIG_I2C=yKconfig3. 在 main.c 中,包含 I2C API 的头文件。
#include <zephyr/drivers/i2c.h>C4. 为了在控制台上显示传感器读数,我们将使用一个简单的 printk()。
4.1 包含头文件 <sys/printk.h> 以使用 printk()。
#include <zephyr/sys/printk.h>C4.2 向 prj.conf 添加以下配置选项以启用对浮点格式说明符的支持,这样我们就可以将温度读数打印为浮点数。默认不启用此选项的原因是为了节省内存空间,因为启用此选项会使代码大小增加至少 1KB。
CONFIG_CBPRINTF_FP_SUPPORT=yKconfig5. 设备树(Devicetree)准备
5.1 创建一个名为 “boards” 的文件夹,并将其放在本练习的目录中,如下所示。

5.2 为开发板添加覆盖文件(overlay file)
如果您使用的是 nRF54L15DK 以外的开发套件,请使用下方选项卡中相应的覆盖内容,并根据您的特定目标板命名覆盖文件。
由于传感器位于板卡外部,我们必须添加一个覆盖文件来指定该外部传感器是哪个 i2c 节点的子节点,并指定传感器的地址。我们还需要启用 i2c 节点并配置引脚。
正如我们在上一主题(I2C 驱动程序)中所解释的,创建一个覆盖文件,命名格式为 [board]_[soc].overlay](对于 nRF54L15 DK,它将是 nrf54l15dk_nrf54l15_cpuapp.overlay),并在覆盖文件中添加以下代码。我们在 0x77 中获取它的方式将在步骤 5.3 中解释。
&i2c21 {
status = "okay";
pinctrl-0 = <&i2c21_default>;
pinctrl-1 = <&i2c21_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c21_default: i2c21_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 11)>,
<NRF_PSEL(TWIM_SDA, 1, 12)>;
};
};
/omit-if-no-ref/ i2c21_sleep: i2c21_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 11)>,
<NRF_PSEL(TWIM_SDA, 1, 12)>;
};
};
};Devicetree&i2c22 {
status = "okay";
pinctrl-0 = <&i2c22_default>;
pinctrl-1 = <&i2c22_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c22_default: i2c22_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 11)>,
<NRF_PSEL(TWIM_SDA, 1, 12)>;
};
};
/omit-if-no-ref/ i2c22_sleep: i2c22_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 11)>,
<NRF_PSEL(TWIM_SDA, 1, 12)>;
low-power-enable;
};
};
};
Devicetree
&i2c21 {
status = "okay";
pinctrl-0 = <&i2c21_default>;
pinctrl-1 = <&i2c21_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c21_default: i2c21_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 11)>,
<NRF_PSEL(TWIM_SDA, 1, 12)>;
};
};
/omit-if-no-ref/ i2c21_sleep: i2c21_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 11)>,
<NRF_PSEL(TWIM_SDA, 1, 12)>;
};
};
};
Devicetree
&i2c21 {
status = "okay";
pinctrl-0 = <&i2c21_default>;
pinctrl-1 = <&i2c21_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c21_default: i2c21_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 11)>,
<NRF_PSEL(TWIM_SDA, 1, 12)>;
};
};
/omit-if-no-ref/ i2c21_sleep: i2c21_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 11)>,
<NRF_PSEL(TWIM_SDA, 1, 12)>;
};
};
};
Devicetree&i2c0 {
status = "okay";
pinctrl-0 = <&i2c0_default>;
pinctrl-1 = <&i2c0_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c0_default: i2c0_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 0, 24)>,
<NRF_PSEL(TWIM_SDA, 0, 25)>;
};
};
/omit-if-no-ref/ i2c0_sleep: i2c0_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 0, 24)>,
<NRF_PSEL(TWIM_SDA, 0, 25)>;
low-power-enable;
};
};
};Devicetree&i2c0 {
status = "okay";
pinctrl-0 = <&i2c0_default>;
pinctrl-1 = <&i2c0_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c0_default: i2c0_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 0, 22)>,
<NRF_PSEL(TWIM_SDA, 0, 23)>;
};
};
/omit-if-no-ref/ i2c0_sleep: i2c0_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 0, 22)>,
<NRF_PSEL(TWIM_SDA, 0, 23)>;
low-power-enable;
};
};
};
Devicetree&i2c0 {
status = "okay";
pinctrl-0 = <&i2c0_default>;
pinctrl-1 = <&i2c0_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c0_default: i2c0_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 14)>,
<NRF_PSEL(TWIM_SDA, 1, 15)>;
};
};
/omit-if-no-ref/ i2c0_sleep: i2c0_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 14)>,
<NRF_PSEL(TWIM_SDA, 1, 15)>;
low-power-enable;
};
};
};
Devicetree&i2c2 {
status = "okay";
pinctrl-0 = <&i2c2_default>;
pinctrl-1 = <&i2c2_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c2_default: i2c2_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 0, 12)>,
<NRF_PSEL(TWIM_SDA, 0, 13)>;
};
};
/omit-if-no-ref/ i2c2_sleep: i2c2_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 0, 12)>,
<NRF_PSEL(TWIM_SDA, 0, 13)>;
low-power-enable;
};
};
};
Devicetree&i2c1 {
status = "okay";
pinctrl-0 = <&i2c1_default>;
pinctrl-1 = <&i2c1_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c1_default: i2c1_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 14)>,
<NRF_PSEL(TWIM_SDA, 1, 15)>;
};
};
/omit-if-no-ref/ i2c1_sleep: i2c1_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 14)>,
<NRF_PSEL(TWIM_SDA, 1, 15)>;
low-power-enable;
};
};
};Devicetree&i2c1 {
status = "okay";
pinctrl-0 = <&i2c1_default>;
pinctrl-1 = <&i2c1_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c1_default: i2c1_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 14)>,
<NRF_PSEL(TWIM_SDA, 1, 15)>;
};
};
/omit-if-no-ref/ i2c1_sleep: i2c1_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 1, 14)>,
<NRF_PSEL(TWIM_SDA, 1, 15)>;
low-power-enable;
};
};
};&i2c2 {
status = "okay";
pinctrl-0 = <&i2c2_default>;
pinctrl-1 = <&i2c2_sleep>;
pinctrl-names = "default", "sleep";
mysensor: mysensor@77{
compatible = "i2c-device";
status = "okay";
reg = < 0x77 >;
};
};
&pinctrl {
/omit-if-no-ref/ i2c2_default: i2c2_default {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 0, 12)>,
<NRF_PSEL(TWIM_SDA, 0, 13)>;
};
};
/omit-if-no-ref/ i2c2_sleep: i2c2_sleep {
group1 {
psels = <NRF_PSEL(TWIM_SCL, 0, 12)>,
<NRF_PSEL(TWIM_SDA, 0, 13)>;
low-power-enable;
};
};
};
Devicetree向应用程序添加覆盖文件时,必须在烧录前运行全新构建(pristine build)。VS Code 中会弹出警告,询问您是否现在运行全新构建。
5.3 7 位设备地址为 111011x。前 6 个 MSB 位是固定的。最后一位可通过 SDO 值更改(参见 bme280 数据手册),并可在运行期间更改。将 SDO 连接到 GND 会得到从机地址 1110110 (0x76);将其连接到 VDD/VDDIO 会得到从机地址 1110111 (0x77),这与 BMP280 的 I²C 地址相同。
SDO 引脚在 Waveshare 15231 板上由 ADDR/MISO 表示。此引脚已上拉,因此如果连接未更改,BME280 中的 SDO 将保持高电平,从而给出 0x77 地址

Waveshare 15231 板 ADDR/MISO 引脚。
一些传感器供应商提供包含读/写位的 8 位地址。为了识别这一点,他们通常提供单独的写入和读取地址。在这种情况下,应仅使用地址的高 7 位。
6. 获取传感器的节点标识符。这在 I2C 驱动程序部分的步骤 4 中有详细解释。
#define I2C_NODE DT_NODELABEL(mysensor)C7. 获取特定于 API 的设备结构,并确保设备已准备就绪:
static const struct i2c_dt_spec dev_i2c = I2C_DT_SPEC_GET(I2C_NODE);
if (!device_is_ready(dev_i2c.bus)) {
printk("I2C bus %s is not ready!\n\r",dev_i2c.bus->name);
return -1;
}C至此,我们有了指向 I2C 控制器设备结构的指针和传感器地址(目标设备地址),可以开始使用 I2C 驱动程序 API 来配置连接到 I2C 控制器的传感器。
8. 定义相关寄存器的地址(来自传感器数据手册)。通常这些信息会放在一个单独的头文件(.h)中。但是,为了保持演示简单,我们将它们添加到 main.c 中。
#define CTRLMEAS 0xF4
#define CALIB00 0x88
#define ID 0xD0
#define TEMPMSB 0xFAC9. 我们通过读取芯片 ID 来验证是否正确连接了传感器。当数值正确时(参见 bme280 数据手册),我们知道传感器工作正常,可以进行下一步 – 读取校准寄存器
uint8_t id = 0;
uint8_t regs[] = {ID};
int ret = i2c_write_read_dt(&dev_i2c, regs, 1, &id, 1);
if (ret != 0) {
printk("Failed to read register %x \n", regs[0]);
return -1;
}
if (id != CHIP_ID) {
printk("Invalid chip id! %x \n", id);
return -1;
}10. 读取校准寄存器。大多数补偿参数是 2 字节值。在这种情况下,我们以突发模式读取温度补偿参数寄存器(每个 2B)。
温度补偿参数从 0x88 地址开始,包含以下 3 个寄存器。

温度校准寄存器
uint8_t values[6];
int ret = i2c_burst_read_dt(spec, CALIB00, values, 6);
if (ret != 0) {
printk("Failed to read register %x \n", CALIB00);
return;
}
从寄存器位置读取后,我们将位/字节放在适当的位置以形成补偿参数值。
sensor_data_ptr->dig_t1 = ((uint16_t)values[1]) << 8 | values[0];
sensor_data_ptr->dig_t2 = ((uint16_t)values[3]) << 8 | values[2];
sensor_data_ptr->dig_t3 = ((uint16_t)values[5]) << 8 | values[4];这些补偿参数将由补偿例程与未补偿的环境读取值一起使用,以生成正确的/补偿后的输出。数据手册中提供了补偿公式和例程。有关更多信息,请参阅数据手册。我们直接使用这些例程,仅对原型和变量数据类型进行微调。
11. 现在我们需要通过写入配置寄存器来配置传感器。根据数据手册,我们必须通过将 MODE 位设置为 b’11 将寄存器置于活动/正常模式。为了将过采样参数设置为 b’100(即 8 倍过采样),我们必须向 CTRLMEAS 寄存器写入 0x93。这些寄存器定义在 main.c 的顶部。

BME280 配置寄存器
uint8_t sensor_config[] = {CTRLMEAS, SENSOR_CONFIG_VALUE};
ret = i2c_write_dt(&dev_i2c, sensor_config, 2);
if (ret != 0) {
printk("Failed to write register %x \n", sensor_config[0]);
return -1;
}C12. 为了获取温度传感器数据,我们需要从 TEMPMSB 开始读取以下 3 个寄存器。
uint8_t temp_val[3] = {0};
int ret = i2c_burst_read_dt(&dev_i2c, TEMPMSB, temp_val, 3);
if (ret != 0) {
printk("Failed to read register %x \n", TEMPMSB);
k_msleep(SLEEP_TIME_MS);
continue;
}C12.1. 我们需要将寄存器中的数据按有效顺序排列,并将它们组合成 20 位温度有符号值(未补偿)
int32_t adc_temp =
(temp_val[0] << 12) | (temp_val[1] << 4) | ((temp_val[2] >> 4) & 0x0F);12.2 接下来,我们可以使用之前存储的参数来补偿温度值
int32_t comp_temp = bme280_compensate_temp(&bmedata, adc_temp);12.3 下一步是将它们转换为摄氏度和华氏度。
float temperature = (float)comp_temp / 100.0f;
double fTemp = (double)temperature * 1.8 + 32;
// Print reading to console
printk("Temperature in Celsius : %8.2f C\n", (double)temperature);
printk("Temperature in Fahrenheit : %.2f F\n", fTemp);13. 使用杜邦线将传感器板连接到您的 DK。
| 导线颜色 | 传感器板 | nRF54L15 DK |
| 红色 | VCC | VDDIO 通过端口 P1 |
| 黑色 | GND | GND 通过端口 P1 |
| 蓝色 | SDA | 端口 P1.12 |
| 黄色 | SCL | 端口 P1.11 |


13.1 配置开发板以输出正确的电压。
在 nRF Connect for Desktop 中,安装并启动 Board Configurator。Board Configurator 是一款桌面应用程序,可让您调整 Nordic 开发套件 (DK) 上“板载控制器”的设置。
板载控制器是运行在 DK 接口 MCU 上的固件,负责控制 DK 的运行。使用 Board Configurator,您可以轻松更改 DK 的配置。
在左上角,点击 Select Device,将出现一个包含所有已连接设备的下拉菜单。选择 nRF54L15 DK。
在 VDD (nPM VOUT1) 下,确保选择了 3.3V。然后选择 Write config,将当前配置写入开发板。现在,开发板将提供正确的输出电压来为扩展板供电。

13. 使用杜邦线将传感器板连接到您的 DK。
| 导线颜色 | 传感器板 | nRF52 DK |
| 红色 | VCC | VDD |
| 黑色 | GND | GND |
| 蓝色 | SDA | 端口 P0.25 |
| 黄色 | SCL | 端口 P0.24 |
13. 使用杜邦线将传感器板连接到您的 DK。
| 导线颜色 | 传感器板 | nRF52833 DK |
| 红色 | VCC | VDD |
| 黑色 | GND | GND |
| 蓝色 | SDA | 端口 P0.23 |
| 黄色 | SCL | 端口 P0.22 |
13. 使用杜邦线将传感器板连接到您的 DK。
| 导线颜色 | 传感器板 | nRF52840 DK |
| 红色 | VCC | VDD |
| 黑色 | GND | GND |
| 蓝色 | SDA | 端口 P1.15 |
| 黄色 | SCL | 端口 P1.14 |
13. 使用杜邦线将传感器板连接到您的 DK。
| 导线颜色 | 传感器板 | nRF5340 DK |
| 红色 | VCC | VDD |
| 黑色 | GND | GND |
| 蓝色 | SDA | 端口 P1.15 |
| 黄色 | SCL | 端口 P1.14 |
13. 使用杜邦线将传感器板连接到您的 DK。
| 导线颜色 | 传感器板 | nRF9160 DK |
| 红色 | VCC | VDD |
| 黑色 | GND | GND |
| 蓝色 | SDA | 端口 P0.13 |
| 黄色 | SCL | 端口 P0.12 |
13. 使用杜邦线将传感器板连接到您的 DK。
| 导线颜色 | 传感器板 | nRF7002 DK |
| 红色 | VCC | VDD |
| 黑色 | GND | GND |
| 蓝色 | SDA | 端口 P1.15 |
| 黄色 | SCL | 端口 P1.14 |
13. 使用杜邦线将传感器板连接到您的 DK。
| 导线颜色 | 传感器板 | nRF91x1 DK |
| 红色 | VCC | VDD |
| 黑色 | GND | GND |
| 蓝色 | SDA | 端口 P0.13 |
| 黄色 | SCL | 端口 P0.12 |
14. 构建应用程序并将其烧录到您的开发套件中。
使用串口终端,您应该看到以下输出:
*** Booting nRF Connect SDK ***
Temperature in Celsius : 26.37 C
Temperature in Fahrenheit : 79.47 F
Temperature in Celsius : 26.37 C
Temperature in Fahrenheit : 79.47 F
Temperature in Celsius : 26.37 C
Temperature in Fahrenheit : 79.47 F
Temperature in Celsius : 26.37 C
Temperature in Fahrenheit : 79.36 F
Temperature in Celsius : 26.37 C
Temperature in Fahrenheit : 79.36 FTerminal尝试对着传感器吹气,观察读数的即时变化。
本练习的解决方案可以在 GitHub 仓库 l6/l6_e1_sol 中找到。