이 글에서는 디지키에서 구매 가능한 Adafruit의 BME280 습도·기압·온도 센서 평가 기판과
Nordic nRF54L15-DK 개발 키트를 연결하여 Zephyr 기반 IoT 기상 관측 시스템을 구현하는 방법을 설명합니다.
이 BME280 센서 평가 기판에는 보쉬의 BME280 센서가 탑재되어 있으며, 본 예제에서는 1MHz 속도의 4선식 SPI 인터페이스를 통해 통신합니다. BME280 센서는 I²C 인터페이스로도 연결할 수 있으며 이 평가 기판은 QWIIC 연결도 제공합니다 - I²C 인터페이스를 사용하는 경우 이전 게시글을 참고하시기 바랍니다. BME280 센서 평가 기판은 핀 헤더를 납땜한 뒤 디지키에서 구매 가능한 이와 유사한 브레드보드에 장착하였습니다.
아래의 색이 구분된 점퍼 와이어를 사용하여 브레드보드와 nRF54L15-DK 개발 키트를 연결하였습니다.
Zephyr 기반 IoT 기상 관측 시스템의 데모를 진행하기 전에, Linux 호스트 환경에서 Zephyr 개발에 필요한 소프트웨어 도구를 설치하는 방법을 설명한 이 게시글을 먼저 참고해 주시기 바랍니다. 아래는 이번 사례에서 사용된 SparkFun 로직 분석기를 설치한 구성으로, 케이블 길이 등의 이유로 최적의 구성은 아닙니다.
본 데모에서 사용된 4선식 SPI 인터페이스 연결과 전원 라인 및 접지는 다음과 같이 요약할 수 있습니다.
아래와 같은 proj.conf라는 이름의 Zephyr 설정 파일을 생성합니다.
CONFIG_GPIO=y
CONFIG_CBPRINTF_FP_SUPPORT=y
CONFIG_SPI=y
CONFIG_LOG=y
다음으로 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)
그리고 4선식 SPI 인터페이스를 정의하는 Zephyr 오버레이 파일인 nrf54l15dk_nrf54l15.overlay도 생성합니다.
&spi21 {
compatible = "nordic,nrf-spim";
status = "okay";
pinctrl-0 = <&spi21_default>;
pinctrl-1 = <&spi21_sleep>;
pinctrl-names = "default", "sleep";
cs-gpios = <&gpio1 8 GPIO_ACTIVE_LOW>;
bme280: bme280@0 {
compatible = "bosch,bme280";
reg = <0>;
spi-max-frequency = <1000000>; // 1MHz
};
};
/* STEP 2.2 - Change the pin configuration */
/* */
&pinctrl {
spi21_default: spi21_default {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 1, 11)>,
<NRF_PSEL(SPIM_MOSI, 1, 13)>,
<NRF_PSEL(SPIM_MISO, 1, 14)>;
};
};
spi21_sleep: spi21_sleep {
group1 {
psels = <NRF_PSEL(SPIM_SCK, 1, 11)>,
<NRF_PSEL(SPIM_MOSI, 1, 13)>,
<NRF_PSEL(SPIM_MISO, 1, 14)>;
low-power-enable;
};
};
};
아래 main.c 파일은 Zephyr 기반 IoT 기상 관측 시스템의 애플리케이션 소스 파일로, 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 *)®s, .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;
}
Zephyr 프로젝트 폴더 구조는 다음과 같아야 합니다.
|-- CMakeLists.txt
|-- nrf54l15dk_nrf54l15.overlay
|-- prj.conf
`-- src
|-- main.c
이제 Python 가상 환경에서 이 Zephyr 프로젝트를 다음과 같이 빌드합니다.
digikey_coffee_cup (venv) $ west build -p always -b nrf54l15dk/nrf54l15/cpuapp -- -DEXTRA_DTC_OVERLAY_FILE=nrf54l15dk_nrf54l15.overlay
마지막으로 nRF54L15-DK 개발 키트를 USB 인터페이스로 연결하여 플래시합니다.
digikey_coffee_cup (venv) $ west flash
이 Zephyr 기반 IoT 기상 관측 시스템 프로그램은 초기화 단계에서 BME280 센서의 독자적인 보정 알고리즘을 수행한 뒤, 센서로부터 습도, 기압, 온도 값을 지속적으로 읽고 초당 한 번 보드의 녹색 LED 상태를 토글합니다. 이 프로그램은 부동소수점 형식의 기상 파라미터를 다양한 단위로 표시하기 위한 고전적인 단위 변환도 함께 수행합니다 (이 때문에 CONFIG_CBPRINTF_FP_SUPPORT=y 설정이 필요합니다).
이제 minicom 터미널을 열어서 nRF54L15-DK 개발 키트가 수집하고 있는 기상 파라미터를 확인할 수 있습니다.
digikey_coffee_cup@digikey:~ $ minicom -D /dev/ttyACM1
4선식 SPI 인터페이스에서 메시지들을 모니터링 하기 위해 SparkFun Logic Analyzer를 사용하였습니다. 첫 번째 스냅샷은 초기화, 보정 단계 과정, 그리고 장치의 보정 레지스터를 읽는 동안 주고받은 모든 메시지를 보여줍니다.
다음은 4선식 SPI 인터페이스 메시지 중 하나를 좀 더 자세히 살펴본 것으로, 이 메시지는 PRESSMSB, PRESSLSB, PRESSXLSB, TEMPMSB, TEMPLSB, TEMPXLSB, HUMMSB, HUMLSB, DUMMY의 9바이트 데이터로 구성되어 있으며, Zephyr 애플리케이션을 실행 중인 nRF54L15-DK 개발 키트가 아래 정의된 주소에서 초당 한 번씩 조회합니다.
#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
MOSI 라인에서 주소를 명확하게 확인할 수 있으며, MISO 라인을 통해 수신된 응답 바이트들도 관찰할 수 있습니다.
Nordic nRF54L15-DK 개발 키트는 전원 제약이 까다로운 다양한 무선 IoT 기상 관측 프로젝트에 활용될 수 있습니다. 앞서 이 글에서 설명된 Nordic 전원 프로파일러를 사용하면 장치의 전력 소비 특성을 분석할 수 있습니다.
nRF54L15 IC는 디지키에서 별도로 구매할 수도 있습니다. 이 Zephyr 기반 IoT 기상 관측 시스템에서 수집된 정보는 필요에 따라 디지키에서 구매할 수 있는 다양한 디스플레이와 연동할 수 있습니다.
그중 한 예시로 SPI 또는 I2C를 통해 연결할 수 있는 아래와 같은 매우 저렴한 검증된 범용 디스플레이가 있으며, 이 외에도 디지키에서 공급하는 다양한 디스플레이 특성을 가진 선택지도 고려할 수 있습니다.
보쉬 BME280 센서는 디지키에서 취급하고 있는 SparkFun의 BME280 평가 기판과 같은 개발 키트에도 포함되어 있습니다.
Zephyr 기반 IoT 기상 관측 시스템에는 Adafruit 풍속 센서와 같은 기상 센서를 추가하여, 아날로그 센서 출력을 nRF54L15의 SAADC(Successive Approximation Analog-to-Digital Converter, 연속 근사형 ADC) 인터페이스를 통해 전처리할 수 있습니다. 풍속 센서의 출력 전압 범위는 0.4 V ~ 2.0 V이며, 풍속으로는 0 m/s ~ 32.4 m/s에 해당합니다. 자세한 내용은 이전 Zephyr SAADC 관련 글을 참고하시기 바랍니다.
AI 기능이 포함된 디지털 저전력 가스, 압력, 온도 및 습도 센서인 BME688과 같은 최신 보쉬 센서들도 Zephyr 기반 IoT 기상 관측 시스템과 연동할 수 있습니다.
이 BME688 센서의 추가 기능과 응용 분야는 다음과 같습니다.
- 실내 공기질 측정
- 박테리아 증식의 지표인 휘발성 황 화합물(VSC) 측정을 통한 구취 또는 음식 부패 감지
- 가스 누출 등 비정상적인 가스 및 냄새 감지
- 아이 돌봄을 위한 기저귀 상태 감지
- 불쾌한 냄새(악취) 조기 감지
마지막으로 보쉬 BME280 센서를 PCB에 장착할 때에는 제조사가 권장하는 리플로우 공정 및 실장 지침을 준수하시기 바랍니다. 이를 준수하지 않으면, 측정 결과가 기댓값에서 벗어날 수 있으며, 이 외에도 고려해야 할 다양한 요인이 존재합니다.
좋은 하루 되세요!
영문 원본: Nordic nRF54L15-DK Zephyr SPI (Temp,Pressure,Humidity) (Zephyr IoT weather station)













