基于 Nordic nRF54L15-DK 开发板的 Zephyr SPI 气象站(温湿度 + 气压监测方案)

本文将介绍如何使用 Nordic nRF54L15-DK 开发套件

搭配在 DigiKey 网站有售的 Adafruit BME280 温湿度 + 气压传感器 breakout 评估板,搭建一套基于 Zephyr 系统的物联网气象站。

Adafruit BME280 温湿度 + 气压传感器 breakout 评估板搭载博世 BME280 传感器,本方案中传感器通过四线 SPI 接口以 1MHz 的速率进行通信。 博世 BME280 传感器同样支持 I2C 接口通信,且该评估板配备了 QWIC 接口(若需使用 I2C 接口方案。实际搭建时,需将 Adafruit BME280 评估板焊接引脚后,安装到面包板上,可选用 DigiKey 在售的面包板

我们使用以下带颜色标识的杜邦线

完成 面包板与 Nordic nRF54L15-DK 开发套件之间的电路连接。

本方案中还使用了 SparkFun 逻辑分析仪进行调试(因连接线过长等因素,该逻辑分析仪并非最优选择),具体硬件连接如图:

四线 SPI 接口连接关系,以及电源和地线连接方式汇总如下表所示:

1. 创建 Zephyr 配置文件

新建名为proj.conf的 Zephyr 配置文件,写入以下内容:

CONFIG_GPIO=y

CONFIG_CBPRINTF_FP_SUPPORT=y

CONFIG_SPI=y

CONFIG_LOG=y

2. 添加工程构建文件

按照 Zephyr 标准开发流程,在工程目录下创建CMakeLists.txt文件,内容如下:

cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(spi)

target_sources(app PRIVATE src/main.c)

3. 编写设备树覆盖文件

创建用于定义四线 SPI 接口的 Zephyr 设备树覆盖文件,命名为nrf54l15dk_nrf54l15.overlay,代码如下:

4. 编写主应用程序代码

创建 Zephyr 气象站物联网应用的主程序文件main.c,该程序每秒读取一次 Adafruit BME280 评估板的温湿度和气压数据,同时控制开发板上的绿色 LED 灯周期性闪烁。代码如下:

/*
 * Copyright (c) 2024 Nordic Semiconductor ASAhttps://www.digikey.com/en/products/filter/lcd-oled-graphic/107?s=N4IgTCBcDaICYEsDOAHANgQwJ5JAXQF8g
 *
 * SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
 */

#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>

/* STEP 1.2 - Include the header files for SPI, GPIO and devicetree */
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/spi.h>

LOG_MODULE_REGISTER(DigiKey_Coffee_Cup, LOG_LEVEL_INF);

#define DELAY_REG 		10
#define DELAY_PARAM		50
#define DELAY_VALUES	1000
#define LED0	13

#define CTRLHUM 		0xF2
#define CTRLMEAS		0xF4
#define CALIB00			0x88
#define CALIB26			0xE1
#define ID				0xD0
#define PRESSMSB		0xF7
#define PRESSLSB		0xF8
#define PRESSXLSB		0xF9
#define TEMPMSB			0xFA
#define TEMPLSB			0xFB
#define TEMPXLSB		0xFC
#define HUMMSB			0xFD
#define HUMLSB			0xFE
#define DUMMY			0xFF

const struct gpio_dt_spec ledspec = GPIO_DT_SPEC_GET(DT_NODELABEL(led0), gpios);

/* STEP 3 - Retrieve the API-device structure */
#define SPIOP	SPI_WORD_SET(8) | SPI_TRANSFER_MSB
struct spi_dt_spec spispec = SPI_DT_SPEC_GET(DT_NODELABEL(bme280), SPIOP, 0);

/* Data structure to store BME280 data */
struct bme280_data {
	/* Compensation parameters */
	uint16_t dig_t1;
	int16_t dig_t2;
	int16_t dig_t3;
	uint16_t dig_p1;Bosch BME 280 sensor
	int16_t dig_p2;
	int16_t dig_p3;
	int16_t dig_p4;
	int16_t dig_p5;
	int16_t dig_p6;
	int16_t dig_p7;
	int16_t dig_p8;
	int16_t dig_p9;
	uint8_t dig_h1;
	int16_t dig_h2;
	uint8_t dig_h3;
	int16_t dig_h4;
	int16_t dig_h5;
	int8_t dig_h6;

	/* Compensated values */
	int32_t comp_temp;
	uint32_t comp_press;
	uint32_t comp_humidity;

	/* Carryover between temperature and pressure/humidity compensation */
	int32_t t_fine;

	uint8_t chip_id;
} bmedata;

static int bme_read_reg(uint8_t reg, uint8_t *data, uint8_t size)
{
	int err;

	/* STEP 4.1 - Set the transmit and receive buffers */
	uint8_t tx_buffer = reg;
	struct spi_buf tx_spi_buf			= {.buf = (void *)&tx_buffer, .len = 1};
	struct spi_buf_set tx_spi_buf_set 	= {.buffers = &tx_spi_buf, .count = 1};
	struct spi_buf rx_spi_bufs 			= {.buf = data, .len = size};
	struct spi_buf_set rx_spi_buf_set	= {.buffers = &rx_spi_bufs, .count = 1};

	/* STEP 4.2 - Call the transceive function */
	err = spi_transceive_dt(&spispec, &tx_spi_buf_set, &rx_spi_buf_set);
	if (err < 0) {
		LOG_ERR("spi_transceive_dt() failed, err: %d", err);
		return err;
	}

	return 0;
}

static int bme_write_reg(uint8_t reg, uint8_t value)
{
	int err;

	/* STEP 5.1 - declare a tx buffer having register address and data */
	uint8_t tx_buf[] = {(reg & 0x7F), value};	
	struct spi_buf 		tx_spi_buf 		= {.buf = tx_buf, .len = sizeof(tx_buf)};
	struct spi_buf_set 	tx_spi_buf_set	= {.buffers = &tx_spi_buf, .count = 1};

	/* STEP 5.2 - call the spi_write_dt function with SPISPEC to write buffers */
	err = spi_write_dt(&spispec, &tx_spi_buf_set);
	if (err < 0) {
		LOG_ERR("spi_write_dt() failed, err %d", err);
		return err;
	}

	return 0;
}

void bme_calibrationdata(void)
{
	/* Set data size of 3 as the first byte is dummy using bme_read_reg() */
	uint8_t values[3];
	uint8_t size = 3;	
	
	uint8_t regaddr;
	LOG_INF("-------------------------------------------------------------");
	LOG_INF("bme_read_calibrationdata: Reading from calibration registers:");
	/* STEP 6 - We are using bme_read_reg() to read required number of bytes from 
	respective register(s) and put values to construct compensation parameters */
	regaddr = CALIB00;
	bme_read_reg(regaddr, values, size);
	bmedata.dig_t1 = ((uint16_t)values[2])<<8 | values[1];
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param T1 = %d", regaddr, size-1, bmedata.dig_t1);
	k_msleep(DELAY_PARAM);

	regaddr += 2;
	bme_read_reg(regaddr, values, size);
	bmedata.dig_t2 = ((uint16_t)values[2])<<8 | values[1];
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param Param T2 = %d", regaddr, size-1, bmedata.dig_t2);
	k_msleep(DELAY_PARAM);
	
	regaddr += 2;
	bme_read_reg(regaddr, values, size);
	bmedata.dig_t3 = ((uint16_t)values[2])<<8 | values[1];
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param T3 = %d", regaddr, size-1, bmedata.dig_t3);
	k_msleep(DELAY_PARAM);
	
	regaddr += 2;
	bme_read_reg(regaddr, values, size);
	bmedata.dig_p1 = ((uint16_t)values[2])<<8 | values[1];
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param P1 = %d", regaddr, size-1, bmedata.dig_p1);
	k_msleep(DELAY_PARAM);

	regaddr += 2;
	bme_read_reg(regaddr, values, size);
	bmedata.dig_p2 = ((uint16_t)values[2])<<8 | values[1];
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param P2 = %d", regaddr, size-1, bmedata.dig_p2);
	k_msleep(DELAY_PARAM);
	
	regaddr += 2;
	bme_read_reg(regaddr, values, size);
	bmedata.dig_p3 = ((uint16_t)values[2])<<8 | values[1];
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param P3 = %d", regaddr, size-1, bmedata.dig_p3);
	k_msleep(DELAY_PARAM);
	
	regaddr += 2;
	bme_read_reg(regaddr, values, size);
	bmedata.dig_p4 = ((uint16_t)values[2])<<8 | values[1];
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param P4 = %d", regaddr, size-1, bmedata.dig_p4);
	k_msleep(DELAY_PARAM);
	
	regaddr += 2;
	bme_read_reg(regaddr, values, size);
	bmedata.dig_p5 = ((uint16_t)values[2])<<8 | values[1];
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param P5 = %d", regaddr, size-1, bmedata.dig_p5);
	k_msleep(DELAY_PARAM);
	
	regaddr += 2;
	bme_read_reg(regaddr, values, size);
	bmedata.dig_p6 = ((uint16_t)values[2])<<8 | values[1];
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param P6 = %d", regaddr, size-1, bmedata.dig_p6);
	k_msleep(DELAY_PARAM);
	
	regaddr += 2;
	bme_read_reg(regaddr, values, size);
	bmedata.dig_p7 = ((uint16_t)values[2])<<8 | values[1];
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param P7 = %d", regaddr, size-1, bmedata.dig_p7);
	k_msleep(DELAY_PARAM);
	
	regaddr += 2;
	bme_read_reg(regaddr, values, size);
	bmedata.dig_p8 = ((uint16_t)values[2])<<8 | values[1];
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param P8 = %d", regaddr, size-1, bmedata.dig_p8);
	k_msleep(DELAY_PARAM);
	
	regaddr += 2;
	bme_read_reg(regaddr, values, size);
	bmedata.dig_p9 = ((uint16_t)values[2])<<8 | values[1];
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param P9 = %d", regaddr, size-1, bmedata.dig_p9);
	k_msleep(DELAY_PARAM);
	
	regaddr += 3; size=2; /* only read one byte for H1 (see datasheet) */
	bme_read_reg(regaddr, values, size);
	bmedata.dig_h1 = (uint8_t)values[1];
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param H1 = %d", regaddr, size-1, bmedata.dig_h1);
	k_msleep(DELAY_PARAM);
	
	regaddr += 64; size=3; /* read two bytes for H2 */
	bme_read_reg(regaddr, values, size);
	bmedata.dig_h2 = ((uint16_t)values[2])<<8 | values[1];
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param H2 = %d", regaddr, size-1, bmedata.dig_h2);
	k_msleep(DELAY_PARAM);
	
	regaddr += 2; size=2; /* only read one byte for H3 */
	bme_read_reg(regaddr, values, size);
	bmedata.dig_h3 = (uint8_t)values[1];
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param H3 = %d", regaddr, size-1, bmedata.dig_h3);
	k_msleep(DELAY_PARAM);
	
	regaddr += 1; size=3; /* read two bytes for H4 */
	bme_read_reg(regaddr, values, size);
	bmedata.dig_h4 = ((uint16_t)values[1])<<4 | (values[2] & 0x0F);
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param H4 = %d", regaddr, size-1, bmedata.dig_h4);
	k_msleep(DELAY_PARAM);
	
	regaddr += 1;
	bme_read_reg(regaddr, values, size);
	bmedata.dig_h5 = ((uint16_t)values[2])<<4 | ((values[1] >> 4) & 0x0F);
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param H5 = %d", regaddr, size-1, bmedata.dig_h5);
	k_msleep(DELAY_PARAM);
	
	regaddr += 2; size=2; /* only read one byte for H6 */
	bme_read_reg(regaddr, values, 2);
	bmedata.dig_h6 = (uint8_t)values[1];
	LOG_INF("\tReg[0x%02x] %d Bytes read: Param H6 = %d", regaddr, size-1, bmedata.dig_h6);
	k_msleep(DELAY_PARAM);
	LOG_INF("-------------------------------------------------------------");

}

int bme_print_registers(void)
{
	uint8_t buf[2];
	uint8_t size = 2;	/* size=2 as 1st byte is dummy using bme_read_reg()  */
	uint8_t data;
	int err;

	/* STEP 7 - Go through the following code and see how we are using
	bme_read_reg() to read and print different registers (1-byte) */

	/* Register addresses to read from (see the data sheet) */
	uint8_t reg_id = ID;
	uint8_t regs_morecalib[16];
	uint8_t regs_more[12];
	
	/* Set the register addresses */
	regs_morecalib[0] = CALIB26;
	for (uint8_t i=0; i<15; i++)
		regs_morecalib[i+1] = regs_morecalib[i] + 1;
	
	regs_more[0] = CTRLHUM;
	for (uint8_t i=0; i<11; i++)
		regs_more[i+1] = regs_more[i] + 1;

	/* Read 1 byte data from each register and print */
	LOG_INF("bme_print_registers: Reading all BME280 registers (one by one)");	
	err = bme_read_reg(reg_id, buf, size);
	if (err < 0)
	{
		LOG_INF("Error in bme_read_reg(), err: %d", err);
		return err;
	}
	
	data = buf[1];
	bmedata.chip_id = data;
	LOG_INF("\tReg[0x%02x]  =  0x%02x", reg_id, data);
	k_msleep(DELAY_REG);
	
	/* Reading from more calibration registers */
	for (uint8_t i = 0; i < sizeof(regs_morecalib); i++)
	{
		err = bme_read_reg(regs_morecalib[i], buf, size);
		if (err < 0)
		{
			LOG_INF("Error in bme_read_reg(), err: %d", err);
			return err;
		}
		data = buf[1];
		LOG_INF("\tReg[0x%02x]  =  0x%02x", regs_morecalib[i], data);
		k_msleep(DELAY_REG);
	}

	/* Reading from more registers */
	for (uint8_t i=0; i<sizeof(regs_more); i++)
	{
		err = bme_read_reg(regs_more[i], buf, size);
		if (err < 0)
		{
			LOG_INF("Error in bme_read_reg(), err: %d", err);
			return err;
		}
		data = buf[1];
		LOG_INF("\tReg[0x%02x]  =  0x%02x", regs_more[i], data);
		k_msleep(DELAY_REG);
	}
	LOG_INF("-------------------------------------------------------------");
	return 0;
}

/* STEP 8 - Go through the compensation functions and 
  note that they use the compensation parameters from 
  the bme280_data structure and then store back the compensated value */
static void bme280_compensate_temp(struct bme280_data *data, int32_t adc_temp)
{
	int32_t var1, var2;

	var1 = (((adc_temp >> 3) - ((int32_t)data->dig_t1 << 1)) *
		((int32_t)data->dig_t2)) >> 11;
	var2 = (((((adc_temp >> 4) - ((int32_t)data->dig_t1)) *
		  ((adc_temp >> 4) - ((int32_t)data->dig_t1))) >> 12) *
		((int32_t)data->dig_t3)) >> 14;

	data->t_fine = var1 + var2;
	data->comp_temp = (data->t_fine * 5 + 128) >> 8;
}

static void bme280_compensate_press(struct bme280_data *data, int32_t adc_press)
{
	int64_t var1, var2, p;

	var1 = ((int64_t)data->t_fine) - 128000;
	var2 = var1 * var1 * (int64_t)data->dig_p6;
	var2 = var2 + ((var1 * (int64_t)data->dig_p5) << 17);
	var2 = var2 + (((int64_t)data->dig_p4) << 35);
	var1 = ((var1 * var1 * (int64_t)data->dig_p3) >> 8) +
		((var1 * (int64_t)data->dig_p2) << 12);
	var1 = (((((int64_t)1) << 47) + var1)) * ((int64_t)data->dig_p1) >> 33;

	/* Avoid exception caused by division by zero */
	if (var1 == 0) {
		data->comp_press = 0U;
		return;
	}

	p = 1048576 - adc_press;
	p = (((p << 31) - var2) * 3125) / var1;
	var1 = (((int64_t)data->dig_p9) * (p >> 13) * (p >> 13)) >> 25;
	var2 = (((int64_t)data->dig_p8) * p) >> 19;
	p = ((p + var1 + var2) >> 8) + (((int64_t)data->dig_p7) << 4);

	data->comp_press = (uint32_t)p;
}

static void bme280_compensate_humidity(struct bme280_data *data, int32_t adc_humidity)
{
	int32_t h;

	h = (data->t_fine - ((int32_t)76800));
	h = ((((adc_humidity << 14) - (((int32_t)data->dig_h4) << 20) -
		(((int32_t)data->dig_h5) * h)) + ((int32_t)16384)) >> 15) *
		(((((((h * ((int32_t)data->dig_h6)) >> 10) * (((h *
		((int32_t)data->dig_h3)) >> 11) + ((int32_t)32768))) >> 10) +
		((int32_t)2097152)) * ((int32_t)data->dig_h2) + 8192) >> 14);
	h = (h - (((((h >> 15) * (h >> 15)) >> 7) *
		((int32_t)data->dig_h1)) >> 4));
	h = (h > 419430400 ? 419430400 : h);

	data->comp_humidity = (uint32_t)(h >> 12);
}

int bme_read_sample(void)
{

	int err;

	int32_t datap = 0, datat = 0, datah = 0;
	float pressure_pa = 0.0f, temperature_c = 0.0f, humidity = 0.0f;

	/* STEP 9.1 - Store register addresses to do burst read */
	uint8_t regs[] = {PRESSMSB, PRESSLSB, PRESSXLSB, \
					  TEMPMSB, TEMPLSB, TEMPXLSB, \
					  HUMMSB, HUMLSB, DUMMY};	//0xFF is dummy reg
	uint8_t readbuf[sizeof(regs)];

	/* STEP 9.2 - Set the transmit and receive buffers */
	struct spi_buf 		tx_spi_buf 			= {.buf = (void *)&regs, .len = sizeof(regs)};
	struct spi_buf_set 	tx_spi_buf_set		= {.buffers = &tx_spi_buf, .count = 1};
	struct spi_buf 		rx_spi_bufs			= {.buf = readbuf, .len = sizeof(regs)};
	struct spi_buf_set 	rx_spi_buf_set	= {.buffers = &rx_spi_bufs, .count = 1};

	/* STEP 9.3 - Use spi_transceive() to transmit and receive at the same time */
	err = spi_transceive_dt(&spispec, &tx_spi_buf_set, &rx_spi_buf_set);
	if (err < 0) {
		LOG_ERR("spi_transceive_dt() failed, err: %d", err);
		return err;
	}

	/* Put the data read from registers into actual order (see datasheet) */
	/* Uncompensated pressure value */
	datap = (readbuf[1] << 12) | (readbuf[2] << 4) | ((readbuf[3] >> 4) & 0x0F);
	/* Uncompensated temperature value */
	datat = (readbuf[4] << 12) | (readbuf[5] << 4) | ((readbuf[6] >> 4) & 0x0F);
	/* Uncompensated humidity value */
	datah = (readbuf[7] << 8)  | (readbuf[8]);

	/* Compensate values using given functions */
	bme280_compensate_press(&bmedata,datap);
	bme280_compensate_temp(&bmedata, datat);
	bme280_compensate_humidity(&bmedata, datah);

	/* Convert to proper format as per data sheet */
	pressure_pa = (float)bmedata.comp_press/256.0f;
    float pressure_in = 0.0002953f * pressure_pa;
	temperature_c = (float)bmedata.comp_temp/100.0f;
    float temperature_f = (1.8f*temperature_c) + 32.0f;
	humidity = (float)bmedata.comp_humidity/1024.0f;

	
	/* Print the uncompensated and compensated values */
	LOG_INF("\tTemperature: \t uncomp = %d C \t comp = %8.2f C", datat, (double)temperature_c);
    LOG_INF("\tTemperature: \t uncomp = %d C \t comp = %8.2f F", datat, (double)temperature_f);
	LOG_INF("\tPressure:    \t uncomp = %d Pa \t comp = %8.2f Pa", datap, (double)pressure_pa);	
    LOG_INF("\tPressure:    \t uncomp = %d Pa \t comp = %8.2f in Hg", datap, (double)pressure_in);	
	LOG_INF("\tHumidity:    \t uncomp = %d RH \t comp = %8.2f %%RH", datah, (double)humidity);

	return 0;
}

int main(void)
{
	int err;
	
	err = gpio_is_ready_dt(&ledspec);
	if (!err) {
		LOG_ERR("Error: GPIO device is not ready, err: %d", err);
		return 0;
	}

	/* STEP 10.1 - Check if SPI and GPIO devices are ready */
	err = spi_is_ready_dt(&spispec);
	if (!err) {
		LOG_ERR("Error: SPI device is not ready, err: %d", err);
		return 0;
	}

	gpio_pin_configure_dt(&ledspec, GPIO_OUTPUT_ACTIVE);
	
	/* STEP 10.2 - Read calibration data */
	bme_calibrationdata();

	/* STEP 10.3 - Write sampling parameters and read and print the registers */
	bme_write_reg(CTRLHUM, 0x04);
	bme_write_reg(CTRLMEAS, 0x93);	
	bme_print_registers();
	
	LOG_INF("Continuously read sensor samples, compensate, and display");

	while(1){
		/* STEP 10.4 - Continuously read sensor samples and toggle led */
		bme_read_sample();
		gpio_pin_toggle_dt(&ledspec);
		k_msleep(DELAY_VALUES);
	}

	return 0;
}

5. 工程目录结构

最终的 Zephyr 工程目录结构应如下所示:

|-- CMakeLists.txt
|-- nrf54l15dk_nrf54l15.overlay
|-- prj.conf
`-- src
    |-- main.c

6. 编译与烧录工程

在 Python 虚拟环境中,执行以下命令编译 Zephyr 工程:

digikey_coffee_cup (venv) $ west build -p always -b nrf54l15dk/nrf54l15/cpuapp -- -DEXTRA_DTC_OVERLAY_FILE=nrf54l15dk_nrf54l15.overlay

最后,通过 USB 接口连接 Nordic nRF54L15-DK 开发板,执行以下命令完成程序烧录:

digikey_coffee_cup (venv) $ west flash

7. 功能说明

这套 Zephyr 物联网气象站程序会在初始化阶段执行博世 BME280 传感器的标准校准流程,随后每秒读取一次传感器的温度、湿度和气压数据,并控制开发板上的绿色 LED 灯同步闪烁。程序还支持将气象数据转换为不同单位的浮点格式进行显示(这也是配置项CONFIG_CBPRINTF_FP_SUPPORT=y的作用)。

程序烧录完成后,可通过以下命令打开 minicom 终端,查看 Nordic nRF54L15-DK 开发板采集到的气象数据:

digikey_coffee_cup@digikey:~ $ minicom -D /dev/ttyACM1

同时,我们使用 SparkFun 逻辑分析仪对四线 SPI 接口的通信数据进行监测。第一张数据快照展示了系统初始化、校准阶段,以及读取传感器校准寄存器时的全部通信报文。

下图是 SPI 接口单次通信报文的详细数据,该次通信中,运行 Zephyr 程序的 Nordic nRF54L15-DK 开发板每秒向传感器发起一次 9 字节的数据查询,查询的寄存器地址如下:

#define PRESSMSB		0xF7
#define PRESSLSB		0xF8
#define PRESSXLSB		0xF9
#define TEMPMSB			0xFA
#define TEMPLSB			0xFB
#define TEMPXLSB		0xFC
#define HUMMSB			0xFD
#define HUMLSB			0xFE
#define DUMMY			0xFF

这些地址信息可在 SPI 的 MOSI(主机发送)总线上清晰观测到,同时 MISO(主机接收)总线上会返回传感器对应寄存器的数值。

Nordic nRF54L15-DK 开发板非常适用于对功耗要求严苛的无线物联网气象站项目。


nRF52L15 芯片可在 DigiKey 网站购买。
若有需要,这套 Zephyr 物联网气象站的数据还可对接 DigiKey 在售的多种显示模块。例如一款成本低廉、支持 SPI 或 I2C 接口的经典显示屏,此外还有多款不同显示规格的模块可供选择。

除本方案使用的传感器外,DigiKey 还提供搭载博世 BME280 传感器的其他开发板,如 SparkFun BME280 开发板

你还可以为这套 Zephyr 物联网气象站扩展更多气象传感器,例如 Adafruit 风速传感器,该传感器可通过 nRF52L15 的 SAADC 接口进行信号调理(其输出电压范围与风速对应:0.4V 对应 0m/s,2.0V 对应 32.4m/s),该传感器同样可在 DigiKey 购买。

DigiKey 还提供博世新款传感器供该气象站方案升级,例如 BME688 传感器。


这是一款集成气体、气压、温度、湿度检测功能的数字低功耗传感器,内置 AI 算法。

博世 BME688 传感器具备更丰富的功能和应用场景,例如:

  • 室内空气质量监测
  • 基于挥发性硫化物检测,判断口臭或食物腐败情况(这类物质是细菌滋生的标志性产物)
  • 异常气体和异味检测,可用于气体泄漏预警
  • 婴儿尿布状态监测
  • 异味和臭味的早期预警

注意事项:请严格遵循博世 BME280 传感器厂商的建议和规范,执行正确的回流焊工艺,并确保传感器在 PCB 上的安装方式符合要求,不当的焊接和安装操作可能导致传感器测量数据偏离预期值。