32 KiB
Описание компонента zh_avr_bme280_sensor
Общие сведения
zh_avr_bme280_sensor — прошивка для AVR-микроконтроллеров ATMega168/328, реализующая беспроводной датчик климата на базе датчика BME280 и трансивера nRF24L01+.
Компонент работает в режиме глубокого сна (Power-down), просыпается по таймеру (watchdog), измеряет температуру, влажность и давление на датчике BME280 (интерфейс I2C) и передаёт на шлюз пакет с идентификатором устройства и уровнем заряда батареи. Идентификатор устройства получается автоматически по воздуху в процедуре сопряжения со шлюзом и сохраняется в EEPROM.
Основные возможности
- Сверхнизкое энергопотребление — около 5 мкА в режиме сна.
- Измерение климата — температура, влажность и атмосферное давление датчиком BME280 по шине I2C.
- Автоматическое сопряжение (pairing) — запрос идентификатора устройства у шлюза по воздуху, хранение ID в EEPROM.
- Передача уровня заряда батареи — измерение напряжения через внутренний АЦП с опорным напряжением 1,1 В.
- Работа по протоколу nRF24L01+ — передача и приём через SPI, адресная доставка на шлюз, динамическая длина пакета.
- Поддержка нескольких МК — ATMega168/168P/168PB/328/328P/328PB.
- Пробуждение по watchdog-таймеру — WDT с интервалом ~8 секунд, цикл работы каждые
WDT_WAKE_COUNT(38) прерываний ≈ 5 минут. - Автономная работа — не требует внешних подтягивающих резисторов (используются внутренние pull-up).
Внутреннее устройство
Структура данных
Компонент не использует динамическое выделение памяти. Сырые показания хранятся в статических переменных, калибровочные коэффициенты BME280 — в массиве bme280_calib[33]. Перед передачей данные сериализуются функцией zh_rf_sensor_build_payload() из внешней библиотеки zh_rf_config в буфер uint8_t[ZH_RF_MAX_PAYLOAD_SIZE] (32 байта) в формате little-endian.
Формат передаваемого пакета
Данные формируются в глобальной переменной transmitted_data типа zh_rf_sensor_transmitted_data_t из библиотеки zh_rf_config, затем сериализуются в полезную нагрузку. Для типа ZH_RF_SENSOR_BME280 полный размер пакета — 12 байт (заголовок id + type + battery и три поля измерений, каждое int16_t в little-endian).
| Поле | Тип | Описание |
|---|---|---|
id |
int16_t |
Идентификатор устройства, назначенный шлюзом при сопряжении |
type |
int16_t |
Тип устройства (ZH_RF_SENSOR_BME280) |
battery |
int16_t |
Напряжение батареи × 100 (например, 330 = 3.30 В) |
value_1 |
int16_t |
Влажность (% × 10, например 453 = 45.3 %) |
value_2 |
int16_t |
Температура (°C × 10, например 253 = 25.3 °C) |
value_3 |
int16_t |
Давление (мм рт. ст. × 10) |
Тип устройства задаётся значением перечисления ZH_RF_SENSOR_BME280 из библиотеки zh_rf_config.
Формат пакета сопряжения
Пакет сопряжения — структура zh_rf_sensor_pairing_data_t (5 байт, ZH_RF_PAIRING_PACKET_SIZE):
| Поле | Тип | Описание |
|---|---|---|
type |
uint8_t |
Тип датчика (код ZH_RF_SENSOR_BME280) |
magic |
uint8_t |
Маркер направления: ZH_RF_PAIRING_REQ_MAGIC (0xA5) в запросе, ZH_RF_PAIRING_ACK_MAGIC (0x5A) в ответе |
token_hi |
uint8_t |
Старший байт псевдослучайного токена |
token_lo |
uint8_t |
Младший байт псевдослучайного токена |
id |
uint8_t |
Идентификатор, назначенный шлюзом (в ответе) |
Конфигурационные макросы
| Макрос | Значение | Описание |
|---|---|---|
GATEWAY_CHANNEL |
120 |
Радиоканал шлюза |
GATEWAY_ADDRESS |
0xDDEEFF |
3-байтовый адрес шлюза |
EEPROM_DEVICE_ID_ADDR |
(uint8_t *)0x00 |
Адрес ячейки EEPROM для хранения ID устройства |
DEVICE_ID_UNASSIGNED |
0xFF |
Признак отсутствующего (несопряжённого) идентификатора |
PAIRING_TIMEOUT_ATTEMPTS |
7 |
Максимальное число попыток сопряжения |
WDT_WAKE_COUNT |
38 |
Количество прерываний WDT до цикла измерения (≈ 5 минут) |
TWI_TWBR |
32 |
Значение регистра TWBR для задания скорости аппаратного TWI |
BME280_ADDR |
0x76 |
7-битный адрес датчика BME280 на шине I2C |
Управление памятью
Компонент не использует динамическую память. Все данные хранятся в статической области и регистрах микроконтроллера. Глобальная переменная transmitted_data заполняется при старте (тип устройства, затем идентификатор из EEPROM или из сопряжения). В цикле обновляются поля battery, value_1 (влажность), value_2 (температура), value_3 (давление). Идентификатор устройства сохраняется в EEPROM адресом 0x00 (eeprom_update_byte).
API (интерфейс)
Все функции являются static и доступны только внутри модуля main.c.
Инициализация
| Функция | Описание |
|---|---|
zh_wdt_start() |
Запуск watchdog-таймера в режиме прерывания с указанным таймаутом (биты WDP3:0) |
zh_wdt_stop() |
Остановка watchdog-таймера |
zh_i2c_init() |
Инициализация аппаратного TWI (I2C) на пинах PC4 (SDA), PC5 (SCL), TWBR = 32 |
zh_bme280_init() |
Инициализация датчика BME280: проверка CHIP_ID, сброс, чтение калибровки, настройка режима |
zh_spi_init() |
Инициализация аппаратного SPI в режиме мастера (Fosc/16) |
zh_nrf24_init() |
Инициализация трансивера nRF24L01+: адреса TX/RX, канал, ретрансмиты, динамические пакеты, CRC |
zh_nrf24_interrupt_init() |
Настройка внешнего прерывания INT0 (PD2, падающий фронт) для сигнала IRQ трансивера |
Идентификатор устройства и сопряжение
| Функция | Описание |
|---|---|
zh_load_device_id() |
Чтение ID устройства из EEPROM (возвращает DEVICE_ID_UNASSIGNED при 0x00/0xFF) |
zh_save_device_id() |
Сохранение ID устройства в EEPROM через eeprom_update_byte |
zh_get_random_token() |
Генерация 16-битного псевдослучайного токена из MCUSR и шума АЦП (никогда не 0) |
zh_do_pairing() |
Неблокирующая процедура сопряжения: запрос → ожидание ACK в RX-режиме → backoff; возвращает назначенный ID |
zh_enter_final_sleep() |
Переход в постоянный сон Power-down до сброса (отключение прерываний, WDT, радио, тактов периферии) |
Измерение
| Функция | Описание |
|---|---|
zh_get_battery_voltage() |
Измерение напряжения батареи через внутренний АЦП с опорой 1,1 В (две конверсии, усреднение) |
Чтение данных BME280
| Функция | Описание |
|---|---|
zh_bme280_read() |
Перевод BME280 в forced mode, чтение 8 сырых байт и распаковка ADC значений температуры, давления, влажности |
zh_bme280_compensate_temperature() |
Компенсация сырой температуры по калибровочным коэффициентам (результат в °C) |
zh_bme280_compensate_pressure() |
Компенсация сырого давления по калибровочным коэффициентам (результат в Па) |
zh_bme280_compensate_humidity() |
Компенсация сырой влажности по калибровочным коэффициентам (результат в %) |
Передача данных nRF24L01+
| Функция | Описание |
|---|---|
zh_nrf24_send() |
Передача буфера: Power Up, FLUSH_TX, W_TX_PAYLOAD, импульс CE, ожидание IRQ с таймаутом ~100 мс, сброс статуса |
zh_nrf24_send_no_wait() |
Запуск передачи без ожидания завершения (импульс CE 15 мкс), завершение сигнализируется IRQ |
zh_nrf24_enter_rx() |
Переход в режим приёма на pipe 0 (PRIM_RX, очистка FIFO, CE) |
zh_nrf24_enter_power_down() |
Перевод трансивера в режим Power Down |
Низкоуровневые SPI-операции
| Функция | Описание |
|---|---|
zh_spi_transfer() |
Обмен одного байта по SPI (полный дуплекс) |
zh_nrf24_write_register() |
Запись одного байта в регистр nRF24L01+ |
zh_nrf24_write_buff_register() |
Запись многобайтового буфера в регистр nRF24L01+ |
zh_nrf24_write_buffer() |
Запись буфера данных с указанной командой (CSN управляется вручную через PB2) |
zh_nrf24_send_command() |
Отправка однобайтовой команды и чтение статуса |
zh_nrf24_read_payload() |
Чтение принятой полезной нагрузки из RX FIFO (R_RX_PAYLOAD) |
zh_nrf24_read_payload_width() |
Чтение длины пакета в RX FIFO (R_RX_PL_WID) |
Низкоуровневые TWI-операции (аппаратный I2C)
| Функция | Описание |
|---|---|
zh_i2c_wait() |
Ожидание флага TWINT с таймаутом 65535 итераций (возвращает false при зависании шины) |
zh_i2c_start() |
Генерация стартового (или повторного стартового) условия, проверка статусов 0x08/0x10 |
zh_i2c_stop() |
Генерация стоп-условия с ограниченным ожиданием сброса бита TWSTO |
zh_i2c_write() |
Передача одного байта через TWDR, проверка статусов ACK (0x18, 0x28, 0x40) |
zh_i2c_read_ack() |
Чтение байта из TWDR с подтверждением ACK (возвращает 0xFF при таймауте) |
zh_i2c_read_nack() |
Чтение последнего байта из TWDR без подтверждения NACK (возвращает 0xFF при таймауте) |
Служебные TWI-функции BME280
| Функция | Описание |
|---|---|
zh_bme280_write_reg() |
Запись одного байта в регистр BME280 (start, адрес, регистр, данные, stop, задержка 2 мс) |
zh_bme280_read_reg() |
Чтение нескольких байт из регистров BME280 (repeated-start, ACK/NACK) |
Потокобезопасность
Компонент работает на bare-metal AVR без RTOS. Точки синхронизации — два ISR:
ISR(WDT_vect)— увеличиваетwdt_wake_count(объявлен какstatic volatile uint8_t, инкремент защищён проверкой верхнего предела), заново включает битWDIE.ISR(INT0_vect)— выставляет флагnrf_event(static volatile uint8_t) при фронте на пине IRQ трансивера.
Главный цикл и машина состояний сопряжения проверяют эти флаги после выхода из сна. Атомарная запись одного байта на AVR гарантирует отсутствие состояний гонки. Весь остальной обмен с периферией (I2C, SPI, АЦП) выполняется последовательно в главном цикле.
Алгоритмы
Алгоритм загрузки идентификатора и сопряжения
- При старте читается ID из EEPROM (
eeprom_read_byte, адрес0x00); значения0x00и0xFFсчитаются признаком несопряжённого устройства. - При отсутствии ID выполняется
zh_do_pairing()— неблокирующая машина состояний (ZH_RF_PAIR_SEND→ZH_RF_PAIR_TX_WAIT→ZH_RF_PAIR_RX_WAIT→ZH_RF_PAIR_BACKOFF):- генерируется токен (
zh_get_random_token()из MCUSR и шума АЦП); - отправляется запрос
zh_rf_sensor_pairing_data_t(magic0xA5) без ожидания (zh_nrf24_send_no_wait); - после TX_DS трансивер переводится в RX-режим, ожидание ответа со сном и WDT-таймаутом 250 мс;
- ответ валидируется по magic
0x5A, типу, токену и ненулевому ID; ширина пакета проверяется черезR_RX_PL_WID; - при неудаче — backoff-пауза ~4 с (WDT), не более
PAIRING_TIMEOUT_ATTEMPTS(7) попыток.
- генерируется токен (
- Назначенный ID сохраняется в EEPROM и записывается в
transmitted_data.id. - После инициализации выполняется пробная сборка пакета
zh_rf_sensor_build_payload(); при нулевом результате (неизвестный тип) — переход в постоянный сон.
Алгоритм инициализации BME280
- Задержка 100 мс после включения.
- Проверка
CHIP_ID(регистрBME280_REG_ID, ожидается0x60). - Сброс записью
0xB6в регистрBME280_REG_RESET, задержка 10 мс. - Чтение калибровочных коэффициентов: 26 байт из
BME280_REG_CALIB00и 7 байт изBME280_REG_CALIB26(всего 33 байта, массивbme280_calib). - Конфигурация влажности:
ctrl_hum = 0x01(oversampling x1). - Конфигурация режима:
config = 0x00(фильтр off, standby off). - Запуск измерения:
ctrl_meas = 0x25(temp/pressure oversampling x16, forced mode) вzh_bme280_read()перед каждым чтением.
Алгоритм компенсации BME280
Сырые ADC-значения пересчитываются в физические величины по алгоритмам производителя из datasheet BME280.
- Температура:
zh_bme280_compensate_temperature()— вычисляетbme280_t_fine(используется для давления и влажности), возвращает результат в °C. - Давление:
zh_bme280_compensate_pressure()— возвращает результат в Па. Приvar1 == 0(знаменатель ноль) возвращает 0.0. - Влажность:
zh_bme280_compensate_humidity()— результат ограничивается диапазоном [0, 419430400] на промежуточном шаге, затем делится на 1024.0 и возвращается в процентах.
Алгоритм аппаратного TWI
- Инициализация:
TWSR = 0x00(предделитель 1),TWBR = TWI_TWBR(32), включение модуля битомTWEN. Скорость:F_CPU / (16 + 2·TWBR·prescaler)≈ 100 кГц при 8 МГц. - Стартовое/повторное стартовое условие: установка
TWSTA, ожиданиеTWINT, проверка статуса (0x08— START,0x10— REPEATED START). - Передача байта: запись в
TWDR, сбросTWINT, ожидание завершения, проверка статусов подтверждения (0x18/0x28— данные,0x40— адрес чтения). - Приём байта: установка
TWEAдля всех байт кроме последнего, чтениеTWDRпослеTWINT. - Останов: установка
TWSTO, ограниченное ожидание (1000 итераций) сброса бита аппаратно. - Обработка ошибок: каждая операция возвращает
falseпри таймауте ожидания или неверном статусе, вызывающая сторона выполняетzh_i2c_stop().
Алгоритм передачи через nRF24L01+
- Переход в Power Up (бит
PWR_UPв CONFIG). - Ожидание 5 мс (время стартования осциллятора).
- Очистка TX FIFO командой
FLUSH_TX. - Загрузка полезной нагрузки командой
W_TX_PAYLOAD(размер пакета определяетzh_rf_sensor_build_payload(), динамические пакеты включены:FEATURE = EN_DPL,DYNPD = DPL_P0). - Импульс CE для запуска передачи.
- Ожидание завершения по пину IRQ (PD2) с таймаутом ~100 мс.
- Сброс флагов статуса, очистка FIFO, переход в Power Down.
Для процедуры сопряжения используется вариант без блокировки (zh_nrf24_send_no_wait, импульс CE 15 мкс) с завершением по прерыванию INT0.
Алгоритм сна и пробуждения
- Инициализация WDT: прерывание по таймауту ~8 секунд (
WDT_T_8S), без сброса MCU. - ISR
WDT_vect: увеличиваетwdt_wake_count(до пределаWDT_WAKE_COUNT), заново включает битWDIE. - Главный цикл: переход в Power-down (
sleep_cpu()). После пробуждения проверяетсяwdt_wake_count >= WDT_WAKE_COUNT(38 × 8 с ≈ 5 минут). - При достижении порога: обнуление счётчика, повторная инициализация BME280 (если датчик недоступен), чтение показаний, сериализация пакета и передача через nRF24L01+.
- Первое измерение выполняется сразу после инициализации и сопряжения, до входа в цикл.
zh_enter_final_sleep()— аварийный постоянный сон: отключение прерываний, WDT, радио, АЦП, TWI, SPI и тактов всей неиспользуемой периферии (PRR).
Примеры использования
Типовой цикл работы (main)
int main(void)
{
transmitted_data.type = ZH_RF_SENSOR_BME280;
MCUSR &= ~(1 << WDRF);
wdt_disable();
cli();
zh_i2c_init();
bme280_present = zh_bme280_init();
zh_spi_init();
zh_nrf24_init();
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sei();
uint8_t device_id = zh_load_device_id();
if (device_id == DEVICE_ID_UNASSIGNED) {
device_id = zh_do_pairing();
}
transmitted_data.id = (int16_t)device_id;
zh_wdt_start(WDT_T_8S);
int32_t adc_t = 0, adc_p = 0, adc_h = 0;
// Первое измерение сразу после инициализации и сопряжения
if (bme280_present == true) {
if (zh_bme280_read(&adc_t, &adc_p, &adc_h) == true) {
transmitted_data.battery =
(int16_t)lroundf(zh_get_battery_voltage() * 100.0);
transmitted_data.value_2 =
(int16_t)lroundf(zh_bme280_compensate_temperature(adc_t) * 10.0);
transmitted_data.value_3 =
(int16_t)lroundf(zh_bme280_compensate_pressure(adc_p) * 0.00750062 * 10.0);
transmitted_data.value_1 =
(int16_t)lroundf(zh_bme280_compensate_humidity(adc_h) * 10.0);
uint8_t payload[ZH_RF_MAX_PAYLOAD_SIZE];
uint8_t size = zh_rf_sensor_build_payload(&transmitted_data,
payload, sizeof(payload));
if (size > 0) {
zh_nrf24_send(payload, size);
}
} else {
bme280_present = false;
}
}
for (;;)
{
sleep_enable();
sleep_cpu();
sleep_disable();
if (wdt_wake_count >= WDT_WAKE_COUNT) {
wdt_wake_count = 0;
if (bme280_present == false) {
bme280_present = zh_bme280_init();
}
if (bme280_present == true) {
if (zh_bme280_read(&adc_t, &adc_p, &adc_h) == true) {
transmitted_data.battery =
(int16_t)lroundf(zh_get_battery_voltage() * 100.0);
transmitted_data.value_2 =
(int16_t)lroundf(zh_bme280_compensate_temperature(adc_t));
transmitted_data.value_3 =
(int16_t)lroundf(zh_bme280_compensate_pressure(adc_p) * 0.00750062);
transmitted_data.value_1 =
(int16_t)lroundf(zh_bme280_compensate_humidity(adc_h));
uint8_t payload[ZH_RF_MAX_PAYLOAD_SIZE];
uint8_t size = zh_rf_sensor_build_payload(&transmitted_data,
payload, sizeof(payload));
if (size > 0) {
zh_nrf24_send(payload, size);
}
} else {
bme280_present = false;
}
}
}
}
}
Изменение конфигурации под другое устройство
// Идентификатор устройства назначается шлюзом при сопряжении
// и хранится в EEPROM (адрес 0x00). Для повторного сопряжения
// обнулите ячейку EEPROM.
#define GATEWAY_CHANNEL 120
#define GATEWAY_ADDRESS 0xDDEEFF
// Тип устройства задаётся перечислением ZH_RF_SENSOR_BME280
Подключение по SPI и I2C (схема выводов)
// Назначение пинов AVR:
// nRF24L01+ (SPI):
// PB2 -> CSN (управление SPI-селектом, выход)
// PB3 -> MOSI (назначается аппаратным SPI)
// PB5 -> SCK (назначается аппаратным SPI)
// PB1 -> CE (управление передачей, выход)
// PD2 -> IRQ (вход, активный низкий, INT0, с подтяжкой к VCC)
// BME280 (I2C, аппаратный TWI):
// PC4 -> SDA (аппаратный TWI, внутренний pull-up)
// PC5 -> SCL (аппаратный TWI, внутренний pull-up)
Логирование
Логирование отсутствует.
Ошибки
Компонент не возвращает коды ошибок. Функции zh_bme280_init, zh_bme280_read, zh_bme280_write_reg, zh_bme280_read_reg возвращают bool (true при успехе). zh_load_device_id возвращает DEVICE_ID_UNASSIGNED при отсутствии сохранённого ID. Остальные функции имеют тип void или возвращают однобайтовый статус SPI (uint8_t).
| Условие | Поведение |
|---|---|
| Нулевое показание АЦП | zh_get_battery_voltage() возвращает 0.0 |
Неверный CHIP_ID BME280 |
zh_bme280_init() возвращает false, флаг bme280_present = false, при следующем цикле предпринимается повторная попытка инициализации |
Таймаут ожидания TWINT |
zh_i2c_wait() возвращает false, операция TWI прерывается |
| Неверный статус TWI (NACK, ошибка шины) | zh_i2c_start() / zh_i2c_write() возвращают false, вызывающая функция выполняет zh_i2c_stop() |
| Таймаут при чтении TWI | zh_i2c_read_ack() / zh_i2c_read_nack() возвращают 0xFF |
| Ошибка чтения BME280 | zh_bme280_read() возвращает false, пакет не формируется, bme280_present = false |
var1 == 0 при компенсации давления |
zh_bme280_compensate_pressure() возвращает 0.0 |
| Превышение таймаута передачи (~100 мс) | Цикл ожидания IRQ прерывается, передача может быть неполной |
| Нет ответа шлюза при сопряжении | После 7 попыток (PAIRING_TIMEOUT_ATTEMPTS) — переход в постоянный сон до сброса |
| Ширина принятого пакета ≠ 5 байт / > 32 байт | FIFO очищается, состояние сопряжения переходит в backoff |
zh_rf_sensor_build_payload() вернул 0 |
Неизвестный тип датчика или слишком мал буфер — переход в постоянный сон |
Ограничения
- Широковещательная передача данных — приём используется только на этапе сопряжения; шлюз не подтверждает пакеты измерений на уровне приложения.
- Отсутствие шифрования — CRC включён, но AES-шифрование не используется. Пакеты могут быть перехвачены.
- Жёстко зашитый адрес шлюза — смена адреса или канала требует перепрошивки (ID устройства при этом хранится в EEPROM).
- Таймаут передачи ~100 мс — если шлюз недоступен, передача прерывается. Аппаратные ретрансмиты nRF24L01+ (
SETUP_RETR = 0xFF: 15 попыток с интервалом до 500 мс) включены. - Точность измерения батареи — зависит от стабильности внутреннего опорного напряжения 1,1 В.
- Программные задержки в TWI — обмен с BME280 выполняется на аппаратном TWI, но ожидание
TWINTреализовано поллингом с таймаутом; прерывания TWI не используются. - Задержка между измерениями ~5 минут — определяется произведением
WDT_WAKE_COUNT(38) на интервал WDT (~8 секунд). В течение этого времени пакет не передаётся. - Давление в мм рт. ст. —
zh_bme280_compensate_pressure()возвращает давление в Паскалях, для перевода в пакет умножается на0.00750062(коэффициент пересчёта Па → мм рт. ст.). - Масштабирование значений ×10 только в первом цикле — при первом измерении температура, влажность и давление умножаются на 10 (одна дробная цифра); в цикле пробуждения значения записываются без масштабирования (см.
main). - Сопряжение требует шлюза — без ответившего шлюза устройство после 7 попыток уходит в постоянный сон до сброса/перепрошивки.
Требования для сборки
- PlatformIO с плагином
platform-atmelavr - AVR-GCC (входит в состав PlatformIO)
- Библиотека
zh_rf_config(веткаdev, подключается черезlib_depsвplatformio.ini) - Программатор USBasp (или совместимый)
- Фьюзы: LFUSE=
0xE2, HFUSE=0xDF, EFUSE=0x07(8 МГц, без делителя)
Установка
Сборка и прошивка через PlatformIO
pio run -e ATmega328P -t upload
Подключение в проекте
Компонент является самодостаточной прошивкой — не подключается как библиотека. Для интеграции в другой проект скопируйте src/main.c, добавьте зависимость zh_rf_config и адаптируйте макросы конфигурации.
; platformio.ini
lib_deps =
https://github.com/aZholtikov/zh_rf_config#dev
// Зависимости: стандартные заголовки AVR-libc, математика и конфигурация протокола
#include "avr/io.h"
#include "avr/interrupt.h"
#include "avr/sleep.h"
#include "avr/eeprom.h"
#include "avr/wdt.h"
#include "util/delay.h"
#include "stdbool.h"
#include "math.h"
#include "zh_rf_config.h"