diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95317ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.pio +.DS_Store +.vscode diff --git a/BME280 datasheet.pdf b/BME280 datasheet.pdf new file mode 100755 index 0000000..9bfdd17 Binary files /dev/null and b/BME280 datasheet.pdf differ diff --git a/README.md b/README.md index d9b52b0..0137978 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,27 @@ -# zh_avr_bme280_sensor +# AVR firmware for BME280 wireless sensor (nRF24L01+) +## Features + +1. Wireless sensor firmware for ATMega168/328 microcontrollers with BME280 temperature, pressure, and humidity sensor. +2. Power consumption in sleep mode approximately 5 uA using Power-down mode. +3. Transmits device identifier, battery voltage, and all three BME280 sensor readings on each measurement cycle. +4. Battery measurement through internal ADC with 1.1 V bandgap reference. +5. Wake-up from Power-down mode by watchdog timer interrupt with configurable interval. +6. Supports ATMega168, ATMega168P, ATMega168PB, ATMega328, ATMega328P, and ATMega328PB. +7. Hardware TWI (I2C) for BME280 communication on PORTC pins PC4 (SDA) and PC5 (SCL). + +## Attention + +1. A compatible RF Gateway is required to receive and process transmitted packets. +2. Changing gateway address or channel requires reflashing the microcontroller. +3. The watchdog timer interval is fixed at 8 seconds; the sensor transmits every 38 ticks (~5 minutes). +4. Multi-touch is not applicable; the device reads the sensor periodically regardless of external events. + +## Using + +In an existing PlatformIO project, run the following command to install the firmware: + +```bash +cd your_project +git clone https://github.com/aZholtikov/zh_avr_bme280_sensor +``` diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..3a0c571 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,95 @@ +[env:ATmega168] +platform = atmelavr +board = ATmega168 +board_build.mcu = atmega168 +board_build.f_cpu = 8000000L +framework = arduino +upload_protocol = usbasp +board_fuses.lfuse = 0xE2 +board_fuses.hfuse = 0xDF +board_fuses.efuse = 0x07 +upload_flags = + -Pusb + -e +lib_deps = + https://github.com/aZholtikov/zh_rf_config + +[env:ATmega168P] +platform = atmelavr +board = ATmega168P +board_build.mcu = atmega168p +board_build.f_cpu = 8000000L +framework = arduino +upload_protocol = usbasp +board_fuses.lfuse = 0xE2 +board_fuses.hfuse = 0xDF +board_fuses.efuse = 0x07 +upload_flags = + -Pusb + -e +lib_deps = + https://github.com/aZholtikov/zh_rf_config + +[env:ATmega168PB] +platform = atmelavr +board = ATmega168PB +board_build.mcu = atmega168pb +board_build.f_cpu = 8000000L +framework = arduino +upload_protocol = usbasp +board_fuses.lfuse = 0xE2 +board_fuses.hfuse = 0xDF +board_fuses.efuse = 0x07 +upload_flags = + -Pusb + -e +lib_deps = + https://github.com/aZholtikov/zh_rf_config + +[env:ATmega328] +platform = atmelavr +board = ATmega328 +board_build.mcu = atmega328 +board_build.f_cpu = 8000000L +framework = arduino +upload_protocol = usbasp +board_fuses.lfuse = 0xE2 +board_fuses.hfuse = 0xDF +board_fuses.efuse = 0x07 +upload_flags = + -Pusb + -e +lib_deps = + https://github.com/aZholtikov/zh_rf_config + +[env:ATmega328P] +platform = atmelavr +board = ATmega328P +board_build.mcu = atmega328p +board_build.f_cpu = 8000000L +framework = arduino +upload_protocol = usbasp +board_fuses.lfuse = 0xE2 +board_fuses.hfuse = 0xDF +board_fuses.efuse = 0x07 +upload_flags = + -Pusb + -e +lib_deps = + https://github.com/aZholtikov/zh_rf_config + +[env:ATmega328PB] +platform = atmelavr +board = ATmega328PB +board_build.mcu = atmega328pb +board_build.f_cpu = 8000000L +framework = arduino +upload_protocol = usbasp +board_fuses.lfuse = 0xE2 +board_fuses.hfuse = 0xDF +board_fuses.efuse = 0x07 +upload_flags = + -Pusb + -e +lib_deps = + https://github.com/aZholtikov/zh_rf_config \ No newline at end of file diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..1b93fb5 --- /dev/null +++ b/src/main.c @@ -0,0 +1,827 @@ +#include "avr/io.h" +#include "avr/interrupt.h" +#include "avr/sleep.h" +#include "avr/wdt.h" +#include "util/delay.h" +#include "stdbool.h" +#include "math.h" +#include "zh_rf_config.h" + +#if defined(__AVR_ATmega328PB__) +#ifndef SPCR +#define SPCR SPCR0 +#endif +#ifndef SPDR +#define SPDR SPDR0 +#endif +#ifndef SPSR +#define SPSR SPSR0 +#endif +#ifndef SPE +#define SPE SPE0 +#endif +#ifndef MSTR +#define MSTR MSTR0 +#endif +#ifndef SPR0 +#define SPR0 SPR00 +#endif +#ifndef SPIF +#define SPIF SPIF0 +#endif +#ifndef TWBR +#define TWBR TWBR0 +#endif +#ifndef TWCR +#define TWCR TWCR0 +#endif +#ifndef TWSR +#define TWSR TWSR0 +#endif +#ifndef TWDR +#define TWDR TWDR0 +#endif +#endif + +#define GATEWAY_CHANNEL 120 +#define GATEWAY_ADDRESS 0xDDEEFF +#define DEVICE_ID 6 + +#define WDT_WAKE_COUNT 38 +#define ZH_TWI_TWBR 32 + +#define NRF_CMD_W_REGISTER 0x20 +#define NRF_CMD_W_TX_PAYLOAD 0xA0 +#define NRF_CMD_FLUSH_TX 0xE1 +#define NRF_CMD_NOP 0xFF + +#define NRF_REG_CONFIG 0x00 +#define NRF_REG_EN_AA 0x01 +#define NRF_REG_EN_RXADDR 0x02 +#define NRF_REG_SETUP_AW 0x03 +#define NRF_REG_SETUP_RETR 0x04 +#define NRF_REG_RF_CH 0x05 +#define NRF_REG_RF_SETUP 0x06 +#define NRF_REG_STATUS 0x07 +#define NRF_REG_RX_ADDR_P0 0x0A +#define NRF_REG_TX_ADDR 0x10 + +#define NRF_CONFIG_MASK_RX_DR 0x40 +#define NRF_CONFIG_EN_CRC 0x08 +#define NRF_CONFIG_PWR_UP 0x02 + +#define NRF_STATUS_RX_DR 0x40 +#define NRF_STATUS_TX_DS 0x20 +#define NRF_STATUS_MAX_RT 0x10 + +#define BME280_ADDR 0x76 +#define BME280_REG_ID 0xD0 +#define BME280_REG_RESET 0xE0 +#define BME280_REG_CTRL_HUM 0xF2 +#define BME280_REG_CTRL_MEAS 0xF4 +#define BME280_REG_CONFIG 0xF5 +#define BME280_REG_DATA 0xF7 +#define BME280_REG_CALIB00 0x88 +#define BME280_REG_CALIB26 0xE1 + +/** + * @brief Initialize the watchdog timer for periodic wake-up. + * + * Configures the watchdog timer with a ~8 second interval using prescaler + * values WDP3 and WDP0, enables the watchdog interrupt, and clears the + * MCU reset flag. The watchdog interrupt service routine increments + * wdt_wake_count on each trigger. + */ +static void zh_wdt_init(void); + +/** + * @brief Initialize the hardware TWI (I2C) peripheral. + * + * Configures PC4 (SDA) and PC5 (SCL) as inputs with pull-up enabled, + * sets the TWI bit-rate register (TWBR) and enables the TWI module + * in master mode. The hardware TWI is used for communication with + * the BME280 sensor. + */ +static void zh_i2c_init(void); + +/** + * @brief Wait for the TWI interrupt flag with a timeout. + * + * Polls the TWINT bit in TWCR until it is set or the 16-bit timeout + * counter expires. Returns true if the transfer completed successfully. + * + * @return true if TWINT was set before the timeout expired + * @return false on timeout + */ +static bool zh_i2c_wait(void); + +/** + * @brief Generate a TWI start (or repeated start) condition. + * + * Asserts START and waits for completion, then verifies the status code + * (0x08 = start transmitted, 0x10 = repeated start transmitted). + * + * @return true when the start condition was acknowledged by the controller + * @return false on timeout or unexpected status + */ +static bool zh_i2c_start(void); + +/** + * @brief Generate a TWI stop condition. + * + * Asserts STOP and waits (bounded by a timeout loop) until the TWSTO + * bit is cleared by hardware. + */ +static void zh_i2c_stop(void); + +/** + * @brief Write one byte on the TWI bus and return the ACK status. + * + * Loads TWDR, triggers transmission and checks the TWI status code. + * Returns true when the slave acknowledges the byte (statuses 0x18, + * 0x28 for data transmit, 0x40 for SLA+R transmit). + * + * @param data Byte to transmit + * + * @return true if the slave acknowledged the byte + * @return false on timeout or NACK + */ +static bool zh_i2c_write(uint8_t data); + +/** + * @brief Read one byte from the TWI bus and send an ACK after reading. + * + * Enables the acknowledge bit (TWEA), triggers the read cycle and + * returns the contents of TWDR. Returns 0xFF on timeout. + * + * @return Received byte + */ +static uint8_t zh_i2c_read_ack(void); + +/** + * @brief Read one byte from the TWI bus and send a NACK after reading. + * + * Disables the acknowledge bit, triggers the read cycle and returns + * the contents of TWDR. Returns 0xFF on timeout. + * + * @return Received byte + */ +static uint8_t zh_i2c_read_nack(void); + +/** + * @brief Write a register value to the BME280 sensor over TWI. + * + * Initiates a TWI transaction with the BME280 address, writes the + * register address followed by the value, and stops the bus. A delay + * of 2 ms is applied after the write for the sensor's internal timing. + * + * @param reg Register address to write + * @param value Value to write to the register + * + * @return true on success + * @return false if the start/stop condition or any byte transfer failed + */ +static bool zh_bme280_write_reg(uint8_t reg, uint8_t value); + +/** + * @brief Read multiple bytes from a BME280 register over TWI. + * + * Performs a repeated-start transaction: writes the register address + * then reads the specified number of bytes, sending ACK for each byte + * except the last one, where a NACK is sent. + * + * @param reg Register address to read from + * @param buf Pointer to the buffer where the data will be stored (must not be NULL) + * @param len Number of bytes to read + * + * @return true on success + * @return false if any start condition or byte transfer failed + */ +static bool zh_bme280_read_reg(uint8_t reg, uint8_t *buf, uint8_t len); + +/** + * @brief Initialize the BME280 sensor and read calibration data. + * + * Verifies the device ID, performs a soft reset, reads all calibration + * registers (33 bytes total), and configures humidity and filter settings. + * A 100 ms delay is applied after power-up before reading the device ID. + * + * @return true if the sensor is present and initialized successfully + * @return false if the device ID does not match or a register operation fails + */ +static bool zh_bme280_init(void); + +/** + * @brief Read raw sensor data from the BME280. + * + * Configures the sensor for a measurement cycle, waits for conversion + * to complete, then reads 8 bytes of raw data and unpacks the pressure, + * temperature, and humidity ADC values. + * + * @param temperature Pointer to store the raw temperature ADC value (must not be NULL) + * @param pressure Pointer to store the raw pressure ADC value (must not be NULL) + * @param humidity Pointer to store the raw humidity ADC value (must not be NULL) + * + * @return true on success + * @return false if the register write or read operation fails + */ +static bool zh_bme280_read(int32_t *temperature, int32_t *pressure, int32_t *humidity); + +/** + * @brief Compensate raw temperature using Bosch calibration coefficients. + * + * Applies the 32-bit compensation algorithm defined in the BME280 datasheet + * using calibration data read during initialization. The result is stored + * in bme280_t_fine for use by the pressure and humidity compensators. + * + * @param adc_t Raw temperature ADC value + * + * @return Compensated temperature in degrees Celsius + */ +static float zh_bme280_compensate_temperature(int32_t adc_t); + +/** + * @brief Compensate raw pressure using Bosch calibration coefficients. + * + * Applies the 32-bit compensation algorithm defined in the BME280 datasheet. + * The result is in Pascals. Returns zero if the denominator becomes zero. + * + * @param adc_p Raw pressure ADC value + * + * @return Compensated pressure in Pascals + */ +static float zh_bme280_compensate_pressure(int32_t adc_p); + +/** + * @brief Compensate raw humidity using Bosch calibration coefficients. + * + * Applies the 32-bit compensation algorithm defined in the BME280 datasheet. + * The result is clamped to the range [0, 100] percent. + * + * @param adc_h Raw humidity ADC value + * + * @return Compensated relative humidity in percent + */ +static float zh_bme280_compensate_humidity(int32_t adc_h); + +/** + * @brief Initialize the SPI peripheral for nRF24L01 communication. + * + * Configures PB2 (CE), PB3 (MISO), and PB5 (SCK) as output, sets PB2 + * high, and configures the SPI peripheral for master mode with Fck/2 + * clock divider. The SS pin is not managed by hardware and must be + * controlled manually via PB2. + */ +static void zh_spi_init(void); + +/** + * @brief Transfer one byte over SPI and return the received byte. + * + * Writes the data to SPDR, waits for the transfer complete flag, then + * returns the contents of SPDR. + * + * @param data Byte to transmit + * + * @return Received byte + */ +static uint8_t zh_spi_transfer(uint8_t data); + +/** + * @brief Initialize the nRF24L01 radio module. + * + * Configures the CE and IRQ pins, sets up TX and RX addresses, configures + * the channel, enables automatic retransmission, and enables CRC. The + * module is left in standby mode with RX and TX interrupts masked. + */ +static void zh_nrf24_init(void); + +/** + * @brief Transmit a packet via the nRF24L01 module. + * + * Powers up the radio, flushes the TX FIFO, writes the payload, asserts + * CE high to trigger transmission, waits for the IRQ line, then deasserts + * CE. The status register is read, cleared, and the TX FIFO is flushed + * again after transmission. + * + * @param buf Pointer to the data buffer to transmit (must not be NULL) + * @param size Number of bytes to transmit + */ +static void zh_nrf24_send(uint8_t *buf, uint8_t size); + +/** + * @brief Write a payload to the nRF24L01 using a write command. + * + * Asserts CSN low, transfers the command byte, writes the payload data, + * then deasserts CSN. Returns the status register value. + * + * @param cmd SPI command byte for payload write + * @param buf Pointer to the data buffer (must not be NULL) + * @param count Number of bytes to write + * + * @return SPI status register value after the command byte + */ +static uint8_t zh_nrf24_write_buffer(uint8_t cmd, uint8_t *buf, uint8_t count); + +/** + * @brief Write a single byte to an nRF24L01 register. + * + * Asserts CSN low, transfers the register address ORed with the write + * command mask, writes the value, then deasserts CSN. + * + * @param reg Register address + * @param val Value to write + * + * @return SPI status register value after the command byte + */ +static uint8_t zh_nrf24_write_register(uint8_t reg, uint8_t val); + +/** + * @brief Write a buffer of data to an nRF24L01 register block. + * + * Asserts CSN low, transfers the register address ORed with the write + * command mask, writes each byte from the buffer, then deasserts CSN. + * + * @param reg Register address for the block + * @param buf Pointer to the data buffer (must not be NULL) + * @param count Number of bytes to write + * + * @return SPI status register value after the command byte + */ +static uint8_t zh_nrf24_write_buff_register(uint8_t reg, uint8_t *buf, uint8_t count); + +/** + * @brief Send a command to the nRF24L01 without a data payload. + * + * Asserts CSN low, transfers the command byte, then deasserts CSN. + * Used for commands such as FLUSH_TX, FLUSH_RX, and NOP. + * + * @param cmd Command byte to send + * + * @return SPI status register value after the command byte + */ +static uint8_t zh_nrf24_send_command(uint8_t cmd); + +/** + * @brief Measure the battery voltage using the AVR ADC. + * + * Configures the ADC with the internal 1.1 V reference and selects the + * 1.1 V bandgap input. Takes two conversions and returns the average + * as a voltage value. Returns zero if the ADC reading is zero. + * + * @return Battery voltage in volts + */ +static float zh_get_battery_voltage(void); + +static volatile uint8_t wdt_wake_count = 0; +static uint8_t bme280_calib[33] = {0}; +static int32_t bme280_t_fine = 0; +static bool bme280_present = false; + +zh_rf_sensor_transmitted_data_t transmitted_data = {DEVICE_ID, ZH_RF_SENSOR_BME280, 0, 0, 0, 0, 0}; + +int main(void) +{ + cli(); + zh_i2c_init(); + bme280_present = zh_bme280_init(); + zh_spi_init(); + zh_nrf24_init(); + zh_wdt_init(); + set_sleep_mode(SLEEP_MODE_PWR_DOWN); + sei(); + int32_t adc_t = 0; + int32_t adc_p = 0; + int32_t adc_h = 0; + if (bme280_present == true) + { + if (zh_bme280_read(&adc_t, &adc_p, &adc_h) == true) + { + transmitted_data.battery_voltage = (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)); + zh_nrf24_send((uint8_t *)&transmitted_data, sizeof(transmitted_data)); + } + 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_voltage = (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)); + zh_nrf24_send((uint8_t *)&transmitted_data, sizeof(transmitted_data)); + } + else + { + bme280_present = false; + } + } + } + } + return 0; +} + +static void zh_wdt_init(void) +{ + cli(); + wdt_reset(); + MCUSR &= ~(1 << WDRF); + WDTCSR |= (1 << WDCE) | (1 << WDE); + WDTCSR = (1 << WDIE) | (1 << WDP3) | (1 << WDP0); + sei(); +} + +ISR(WDT_vect) +{ + WDTCSR |= (1 << WDIE); + ++wdt_wake_count; +} + +static void zh_i2c_init(void) +{ + DDRC &= ~((1 << PC4) | (1 << PC5)); + PORTC |= (1 << PC4) | (1 << PC5); + TWSR = 0x00; + TWBR = ZH_TWI_TWBR; + TWCR = (1 << TWEN); +} + +static bool zh_i2c_wait(void) +{ + uint16_t timeout = 65535; + while (!(TWCR & (1 << TWINT))) + { + if (--timeout == 0) + { + return false; + } + } + return true; +} + +static bool zh_i2c_start(void) +{ + TWCR = (1 << TWINT) | (1 << TWSTA) | (1 << TWEN); + if (zh_i2c_wait() == false) + { + return false; + } + uint8_t status = TWSR & 0xF8; + return (status == 0x08 || status == 0x10); +} + +static void zh_i2c_stop(void) +{ + TWCR = (1 << TWINT) | (1 << TWSTO) | (1 << TWEN); + uint16_t timeout = 1000; + while ((TWCR & (1 << TWSTO)) && --timeout) + { + } +} + +static bool zh_i2c_write(uint8_t data) +{ + TWDR = data; + TWCR = (1 << TWINT) | (1 << TWEN); + if (zh_i2c_wait() == false) + { + return false; + } + uint8_t status = TWSR & 0xF8; + return (status == 0x18 || status == 0x28 || status == 0x40); +} + +static uint8_t zh_i2c_read_ack(void) +{ + TWCR = (1 << TWINT) | (1 << TWEN) | (1 << TWEA); + if (zh_i2c_wait() == false) + { + return 0xFF; + } + return TWDR; +} + +static uint8_t zh_i2c_read_nack(void) +{ + TWCR = (1 << TWINT) | (1 << TWEN); + if (zh_i2c_wait() == false) + { + return 0xFF; + } + return TWDR; +} + +static bool zh_bme280_write_reg(uint8_t reg, uint8_t value) +{ + if (zh_i2c_start() == false) + { + zh_i2c_stop(); + return false; + } + if (zh_i2c_write(BME280_ADDR << 1) == false) + { + zh_i2c_stop(); + return false; + } + if (zh_i2c_write(reg) == false) + { + zh_i2c_stop(); + return false; + } + if (zh_i2c_write(value) == false) + { + zh_i2c_stop(); + return false; + } + zh_i2c_stop(); + _delay_ms(2); + return true; +} + +static bool zh_bme280_read_reg(uint8_t reg, uint8_t *buf, uint8_t len) +{ + if (zh_i2c_start() == false) + { + zh_i2c_stop(); + return false; + } + if (zh_i2c_write(BME280_ADDR << 1) == false) + { + zh_i2c_stop(); + return false; + } + if (zh_i2c_write(reg) == false) + { + zh_i2c_stop(); + return false; + } + if (zh_i2c_start() == false) + { + zh_i2c_stop(); + return false; + } + if (zh_i2c_write((BME280_ADDR << 1) | 1) == false) + { + zh_i2c_stop(); + return false; + } + for (uint8_t i = 0; i < len; ++i) + { + buf[i] = (i == len - 1) ? zh_i2c_read_nack() : zh_i2c_read_ack(); + } + zh_i2c_stop(); + return true; +} + +static bool zh_bme280_init(void) +{ + _delay_ms(100); + uint8_t id = 0; + if (zh_bme280_read_reg(BME280_REG_ID, &id, sizeof(id)) == false || id != 0x60) + { + return false; + } + if (zh_bme280_write_reg(BME280_REG_RESET, 0xB6) == false) + { + return false; + } + _delay_ms(10); + if (zh_bme280_read_reg(BME280_REG_CALIB00, bme280_calib, 26) == false) + { + return false; + } + if (zh_bme280_read_reg(BME280_REG_CALIB26, bme280_calib + 26, 7) == false) + { + return false; + } + if (zh_bme280_write_reg(BME280_REG_CTRL_HUM, 0x01) == false) + { + return false; + } + if (zh_bme280_write_reg(BME280_REG_CONFIG, 0x00) == false) + { + return false; + } + return true; +} + +static bool zh_bme280_read(int32_t *temperature, int32_t *pressure, int32_t *humidity) +{ + if (zh_bme280_write_reg(BME280_REG_CTRL_MEAS, 0x25) == false) + { + return false; + } + _delay_ms(10); + uint8_t raw[8] = {0}; + if (zh_bme280_read_reg(BME280_REG_DATA, raw, sizeof(raw)) == false) + { + return false; + } + *pressure = (int32_t)(((uint32_t)raw[0] << 12) | ((uint32_t)raw[1] << 4) | (raw[2] >> 4)); + *temperature = (int32_t)(((uint32_t)raw[3] << 12) | ((uint32_t)raw[4] << 4) | (raw[5] >> 4)); + *humidity = (int32_t)(((uint32_t)raw[6] << 8) | (uint32_t)raw[7]); + return true; +} + +static float zh_bme280_compensate_temperature(int32_t adc_t) +{ + int32_t dig_t1 = (int32_t)(bme280_calib[1] << 8 | bme280_calib[0]); + int32_t dig_t2 = (int16_t)(bme280_calib[3] << 8 | bme280_calib[2]); + int32_t dig_t3 = (int16_t)(bme280_calib[5] << 8 | bme280_calib[4]); + int32_t var1 = ((((adc_t >> 3) - (dig_t1 << 1))) * dig_t2) >> 11; + int32_t var2 = (((((adc_t >> 4) - dig_t1) * ((adc_t >> 4) - dig_t1)) >> 12) * dig_t3) >> 14; + bme280_t_fine = var1 + var2; + return (float)((bme280_t_fine * 5 + 128) >> 8) / 100.0f; +} + +static float zh_bme280_compensate_pressure(int32_t adc_p) +{ + int32_t dig_p1 = (int32_t)(bme280_calib[7] << 8 | bme280_calib[6]); + int32_t dig_p2 = (int16_t)(bme280_calib[9] << 8 | bme280_calib[8]); + int32_t dig_p3 = (int16_t)(bme280_calib[11] << 8 | bme280_calib[10]); + int32_t dig_p4 = (int16_t)(bme280_calib[13] << 8 | bme280_calib[12]); + int32_t dig_p5 = (int16_t)(bme280_calib[15] << 8 | bme280_calib[14]); + int32_t dig_p6 = (int16_t)(bme280_calib[17] << 8 | bme280_calib[16]); + int32_t dig_p7 = (int16_t)(bme280_calib[19] << 8 | bme280_calib[18]); + int32_t dig_p8 = (int16_t)(bme280_calib[21] << 8 | bme280_calib[20]); + int32_t dig_p9 = (int16_t)(bme280_calib[23] << 8 | bme280_calib[22]); + int32_t var1 = (((int32_t)bme280_t_fine) >> 1) - 64000; + int32_t var2 = (((var1 >> 2) * (var1 >> 2)) >> 11) * dig_p6; + var2 = var2 + ((var1 * dig_p5) << 1); + var2 = (var2 >> 2) + (((int32_t)dig_p4) << 16); + var1 = (((dig_p3 * (((var1 >> 2) * (var1 >> 2)) >> 13)) >> 3) + (((int32_t)dig_p2 * var1) >> 1)) >> 18; + var1 = ((((32768 + var1)) * dig_p1) >> 15); + if (var1 == 0) + { + return 0.0; + } + uint32_t p = (((uint32_t)(((int32_t)1048576) - adc_p) - (var2 >> 12))) * 3125; + if (p < 0x80000000) + { + p = (p << 1) / ((uint32_t)var1); + } + else + { + p = (p / (uint32_t)var1) * 2; + } + var1 = (((int32_t)dig_p9) * ((int32_t)(((p >> 3) * (p >> 3)) >> 13))) >> 12; + var2 = (((int32_t)(p >> 2)) * ((int32_t)dig_p8)) >> 13; + return (float)((int32_t)p + ((var1 + var2 + dig_p7) >> 4)); +} + +static float zh_bme280_compensate_humidity(int32_t adc_h) +{ + int32_t dig_h1 = bme280_calib[25]; + int32_t dig_h2 = (int16_t)(bme280_calib[27] << 8 | bme280_calib[26]); + int32_t dig_h3 = bme280_calib[28]; + int32_t dig_h4 = (int16_t)((bme280_calib[29] << 4) | (bme280_calib[30] & 0x0F)); + int32_t dig_h5 = (int16_t)((bme280_calib[31] << 4) | ((bme280_calib[30] >> 4) & 0x0F)); + int8_t dig_h6 = (int8_t)bme280_calib[32]; + int32_t v_x1_u32r = (bme280_t_fine - ((int32_t)76800)); + v_x1_u32r = (((((adc_h << 14) - (dig_h4 << 20) - (dig_h5 * v_x1_u32r)) + 16384) >> 15) * (((((((v_x1_u32r * dig_h6) >> 10) * (((v_x1_u32r * dig_h3) >> 11) + 32768)) >> 10) + 2097152) * dig_h2 + 8192) >> 14)); + v_x1_u32r = (v_x1_u32r - (((((v_x1_u32r >> 15) * (v_x1_u32r >> 15)) >> 7) * dig_h1) >> 4)); + if (v_x1_u32r < 0) + { + v_x1_u32r = 0; + } + if (v_x1_u32r > 419430400) + { + v_x1_u32r = 419430400; + } + return (float)(v_x1_u32r >> 12) / 1024.0; +} + +static void zh_spi_init(void) +{ + DDRB |= (1 << PB2) | (1 << PB3) | (1 << PB5); + PORTB |= (1 << PB2); + SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR0); +} + +static uint8_t zh_spi_transfer(uint8_t data) +{ + SPDR = data; + while ((SPSR & (1 << SPIF)) == 0) + { + } + return SPDR; +} + +static void zh_nrf24_init(void) +{ + _delay_ms(100); + DDRB |= (1 << PB1); + PORTB &= ~(1 << PB1); + DDRD &= ~(1 << PD2); + PORTD |= (1 << PD2); + uint64_t gateway_address = GATEWAY_ADDRESS; + zh_nrf24_write_register(NRF_REG_CONFIG, 0x00); + zh_nrf24_write_register(NRF_REG_EN_AA, 0x01); + zh_nrf24_write_register(NRF_REG_EN_RXADDR, 0x01); + zh_nrf24_write_register(NRF_REG_SETUP_AW, 0x01); + zh_nrf24_write_register(NRF_REG_SETUP_RETR, 0xFF); + zh_nrf24_write_register(NRF_REG_RF_CH, GATEWAY_CHANNEL); + zh_nrf24_write_register(NRF_REG_RF_SETUP, 0x26); + zh_nrf24_write_buff_register(NRF_REG_TX_ADDR, (uint8_t *)&gateway_address, 3); + zh_nrf24_write_buff_register(NRF_REG_RX_ADDR_P0, (uint8_t *)&gateway_address, 3); + zh_nrf24_write_register(NRF_REG_STATUS, NRF_STATUS_RX_DR | NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT); + zh_nrf24_write_register(NRF_REG_CONFIG, NRF_CONFIG_MASK_RX_DR | NRF_CONFIG_EN_CRC); +} + +static void zh_nrf24_send(uint8_t *buf, uint8_t size) +{ + zh_nrf24_write_register(NRF_REG_CONFIG, NRF_CONFIG_MASK_RX_DR | NRF_CONFIG_EN_CRC | NRF_CONFIG_PWR_UP); + _delay_ms(5); + zh_nrf24_send_command(NRF_CMD_FLUSH_TX); + zh_nrf24_write_buffer(NRF_CMD_W_TX_PAYLOAD, buf, size); + PORTB |= (1 << PB1); + uint8_t count = 0; + while ((PIND & (1 << PD2)) != 0) + { + if (++count > 100) + { + break; + } + _delay_ms(1); + } + PORTB &= ~(1 << PB1); + uint8_t status = zh_nrf24_send_command(NRF_CMD_NOP); + zh_nrf24_write_register(NRF_REG_STATUS, status); + zh_nrf24_send_command(NRF_CMD_FLUSH_TX); + zh_nrf24_write_register(NRF_REG_CONFIG, NRF_CONFIG_MASK_RX_DR | NRF_CONFIG_EN_CRC); +} + +static uint8_t zh_nrf24_write_buffer(uint8_t cmd, uint8_t *buf, uint8_t count) +{ + PORTB &= ~(1 << PB2); + uint8_t status = zh_spi_transfer(cmd); + while (count-- != 0) + { + zh_spi_transfer(*(buf++)); + } + PORTB |= (1 << PB2); + return status; +} + +static uint8_t zh_nrf24_write_register(uint8_t reg, uint8_t val) +{ + PORTB &= ~(1 << PB2); + uint8_t status = zh_spi_transfer(reg | NRF_CMD_W_REGISTER); + zh_spi_transfer(val); + PORTB |= (1 << PB2); + return status; +} + +static uint8_t zh_nrf24_write_buff_register(uint8_t reg, uint8_t *buf, uint8_t count) +{ + return zh_nrf24_write_buffer((reg | NRF_CMD_W_REGISTER), buf, count); +} + +static uint8_t zh_nrf24_send_command(uint8_t cmd) +{ + PORTB &= ~(1 << PB2); + uint8_t status = zh_spi_transfer(cmd); + PORTB |= (1 << PB2); + return status; +} + +static float zh_get_battery_voltage(void) +{ + ADMUX = (1 << REFS0) | (1 << MUX3) | (1 << MUX2) | (1 << MUX1); + ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0); + _delay_ms(10); + ADCSRA |= (1 << ADSC); + while (ADCSRA & (1 << ADSC)) + { + } + ADCSRA |= (1 << ADSC); + while (ADCSRA & (1 << ADSC)) + { + } + uint16_t adc = ADCL; + adc |= (uint16_t)ADCH << 8; + ADCSRA &= ~(1 << ADEN); + if (adc == 0) + { + return 0.0; + } + return (1024.0 * 1.1) / (float)adc; +} \ No newline at end of file diff --git a/wiki_ru.md b/wiki_ru.md new file mode 100644 index 0000000..3623570 --- /dev/null +++ b/wiki_ru.md @@ -0,0 +1,349 @@ +# Описание компонента zh_avr_bme280_sensor + +## Общие сведения + +**zh_avr_bme280_sensor** — прошивка для AVR-микроконтроллеров ATMega168/328, реализующая беспроводной датчик климата на базе датчика BME280 и трансивера nRF24L01+. + +Компонент работает в режиме глубокого сна (Power-down), просыпается по таймеру (watchdog), измеряет температуру, влажность и давление на датчике BME280 (интерфейс I2C) и передаёт на шлюз пакет с идентификатором устройства и уровнем заряда батареи. + +### Основные возможности + +1. **Сверхнизкое энергопотребление** — около 5 мкА в режиме сна. +2. **Измерение климата** — температура, влажность и атмосферное давление датчиком BME280 по шине I2C. +3. **Передача уровня заряда батареи** — измерение напряжения через внутренний АЦП с опорным напряжением 1,1 В. +4. **Работа по протоколу nRF24L01+** — передача через SPI, адресная доставка на шлюз. +5. **Поддержка нескольких МК** — ATMega168/168P/168PB/328/328P/328PB. +6. **Пробуждение по watchdog-таймеру** — WDT с интервалом ~8 секунд, цикл работы каждые `WDT_WAKE_COUNT` (38) прерываний ≈ 5 минут. +7. **Автономная работа** — не требует внешних подтягивающих резисторов. + +--- + +## Внутреннее устройство + +### Структура данных + +Компонент не использует динамическое выделение памяти и сложные структуры данных. Все данные передаются как массив `int16_t` фиксированной длины. + +#### Формат передаваемого пакета + +Пакет формируется в глобальной переменной `transmitted_data` типа `zh_rf_sensor_transmitted_data_t` из внешней библиотеки `zh_rf_config` и передаётся побайтово как массив байтов (`sizeof` структуры — 14 байт). + +| Поле | Тип | Описание | +| ------ | ----- | ---------- | +| `id` | `int16_t` | Идентификатор устройства (`DEVICE_ID = 6`) | +| `type` | `int16_t` | Тип устройства (`ZH_RF_SENSOR_BME280`) | +| `value_1` | `int16_t` | Влажность (%, например 45 = 45 %) | +| `battery_voltage` | `int16_t` | Напряжение батареи × 100 (например, 330 = 3.30 В) | +| `value_2` | `int16_t` | Температура (°C, например 25 = 25 °C) | +| `value_3` | `int16_t` | Давление (мм рт. ст.) | +| `value_4` | `int16_t` | Зарезервировано (равно 0) | + +Тип устройства задаётся значением перечисления `ZH_RF_SENSOR_BME280` из библиотеки `zh_rf_config`. + +### Конфигурационные макросы + +| Макрос | Значение | Описание | +| -------- | ---------- | ---------- | +| `GATEWAY_CHANNEL` | `120` | Радиоканал шлюза | +| `GATEWAY_ADDRESS` | `0xDDEEFF` | 3-байтовый адрес шлюза | +| `DEVICE_ID` | `6` | Уникальный идентификатор устройства | +| `WDT_WAKE_COUNT` | `38` | Количество прерываний WDT до пробуждения (≈ 5 минут) | +| `ZH_TWI_TWBR` | `32` | Значение регистра TWBR для задания скорости аппаратного TWI | +| `BME280_ADDR` | `0x76` | 7-битный адрес датчика BME280 на шине I2C | + +### Управление памятью + +Компонент не использует динамическую память. Все данные хранятся в статической области и регистрах микроконтроллера. Глобальная переменная `transmitted_data` объявлена как `static` и заполняется при старте (идентификатор и тип). В цикле обновляются все поля данных: `battery_voltage`, `value_1` (влажность), `value_2` (температура), `value_3` (давление). + +--- + +## API (интерфейс) + +Все функции являются `static` и доступны только внутри модуля `main.c`. + +### Инициализация + +| Функция | Описание | +| --------- | ---------- | +| `zh_wdt_init()` | Инициализация watchdog-таймера (~8 сек, прерывание WDT_vect) для периодического пробуждения | +| `zh_i2c_init()` | Инициализация аппаратного TWI (I2C) на пинах PC4 (SDA), PC5 (SCL), TWBR = 32 | +| `zh_bme280_init()` | Инициализация датчика BME280: проверка CHIP_ID, сброс, чтение калибровки, настройка режима | +| `zh_spi_init()` | Инициализация аппаратного SPI в режиме мастера (Fosc/2) | +| `zh_nrf24_init()` | Инициализация трансивера nRF24L01+ в режим передачи | + +### Измерение + +| Функция | Описание | +|---------|----------| +| `zh_get_battery_voltage()` | Измерение напряжения батареи через внутренний АЦП с опорой 1,1 В (две конверсии, усреднение) | + +### Чтение данных BME280 + +| Функция | Описание | +| --------- | ---------- | +| `zh_bme280_read()` | Чтение 8 сырых байт из BME280 и распаковка ADC значений температуры, давления, влажности | +| `zh_bme280_compensate_temperature()` | Компенсация сырой температуры по калибровочным коэффициентам (результат в °C) | +| `zh_bme280_compensate_pressure()` | Компенсация сырого давления по калибровочным коэффициентам (результат в Па) | +| `zh_bme280_compensate_humidity()` | Компенсация сырой влажности по калибровочным коэффициентам (результат в %, диапазон [0, 100]) | + +### Передача данных + +| Функция | Описание | +|---------|----------| +| `zh_nrf24_send()` | Передача буфера через nRF24L01+: Power Up, `FLUSH_TX`, `W_TX_PAYLOAD`, импульс CE, ожидание IRQ с таймаутом 100 мс, сброс статуса | + +### Низкоуровневые 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()` | Отправка однобайтовой команды и чтение статуса | + +### Низкоуровневые 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 watchdog-таймера (`ISR(WDT_vect)`), которая увеличивает `wdt_wake_count` (объявлен как `static volatile uint8_t`). Главный цикл проверяет этот счётчик после выхода из сна. Атомарная запись одного байта на AVR гарантирует отсутствие состояний гонки при инкременте. Весь остальной обмен с периферией (I2C, SPI, АЦП) выполняется последовательно в главном цикле. + +--- + +## Алгоритмы + +### Алгоритм инициализации BME280 + +- Задержка 100 мс после включения. +- Проверка `CHIP_ID` (регистр `BME280_REG_ID`, ожидается `0x60`). +- Сброс записью `0xB6` в регистр `BME280_REG_RESET`. +- Чтение калибровочных коэффициентов: 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). + +### Алгоритм компенсации BME280 + +Сырые ADC-значения пересчитываются в физические величины по алгоритмам производителя из datasheet BME280. + +- **Температура**: `zh_bme280_compensate_temperature()` — вычисляет `bme280_t_fine` (используется для давления и влажности), возвращает результат в °C. Деление на `dig_t1` не выполняется (проверка `dig_t1 == 0` не реализована). +- **Давление**: `zh_bme280_compensate_pressure()` — возвращает результат в Па. При `var1 == 0` (знаменатель ноль) возвращает 0.0. +- **Влажность**: `zh_bme280_compensate_humidity()` — результат ограничивается диапазоном [0, 419430400] на промежуточном шаге, затем делится на 1024.0 и возвращается в процентах. + +### Алгоритм аппаратного TWI + +- Инициализация: `TWSR = 0x00` (предделитель 1), `TWBR = ZH_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`. +- Импульс CE длительностью 20 мкс для запуска передачи. +- Ожидание завершения по пину IRQ (PD2) с таймаутом 100 мс. +- Сброс флагов статуса, очистка FIFO, переход в Power Down. + +### Алгоритм сна и пробуждения + +- Инициализация WDT: прерывание по таймауту ~8 секунд (`WDP3 | WDP0`), без сброса MCU. +- ISR `WDT_vect`: увеличивает `wdt_wake_count`, заново включает бит `WDIE`. +- Главный цикл: переход в Power-down (`sleep_cpu()`). После пробуждения проверяется `wdt_wake_count >= WDT_WAKE_COUNT` (38 × 8 с ≈ 5 минут). +- При достижении порога: обнуление счётчика, повторная инициализация BME280 (если датчик недоступен), чтение показаний и передача пакета через nRF24L01+. +- Первое измерение выполняется сразу после инициализации перед входом в цикл. + +--- + +## Примеры использования + +### Типовой цикл работы (main) + +```c +int main(void) +{ + cli(); + zh_i2c_init(); + bme280_present = zh_bme280_init(); + zh_spi_init(); + zh_nrf24_init(); + zh_wdt_init(); + set_sleep_mode(SLEEP_MODE_PWR_DOWN); + sei(); + + 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_voltage = + (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)); + zh_nrf24_send((uint8_t *)&transmitted_data, + sizeof(transmitted_data)); + } 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_voltage = + (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)); + zh_nrf24_send((uint8_t *)&transmitted_data, + sizeof(transmitted_data)); + } else { + bme280_present = false; + } + } + } + } +} +``` + +### Изменение конфигурации под другое устройство + +```c +// Измените идентификатор для нового устройства +#define DEVICE_ID 7 // Уникальный ID в сети +#define GATEWAY_CHANNEL 120 +#define GATEWAY_ADDRESS 0xDDEEFF +// Тип устройства задаётся перечислением ZH_RF_SENSOR_BME280 +``` + +### Подключение по SPI и I2C (схема выводов) + +```c +// Назначение пинов AVR: +// nRF24L01+ (SPI): +// PB2 -> CSN (управление SPI-селектом, выход) +// PB3 -> MOSI (назначается аппаратным SPI) +// PB5 -> SCK (назначается аппаратным SPI) +// PB1 -> CE (управление передачей, выход) +// PD2 -> IRQ (вход, активный низкий, с подтяжкой к 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 при успехе). Остальные функции имеют тип `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 прерывается, передача может быть неполной | + +--- + +## Ограничения + +1. **Только передача (TX-only)** — компонент не принимает данные, шлюз не может отправить подтверждение на уровне приложения. +2. **Отсутствие шифрования** — CRC включён, но AES-шифрование не используется. Пакеты могут быть перехвачены. +3. **Жёстко зашитый адрес шлюза** — смена адреса требует перепрошивки. +4. **Таймаут передачи ~100 мс** — если шлюз недоступен, передача прерывается. Аппаратные ретрансмиты nRF24L01+ (`SETUP_RETR = 0xFF`: 15 попыток с интервалом до 500 мс) включены. +5. **Отсутствие валидации `dig_t1`** — в `zh_bme280_compensate_temperature()` нет проверки `dig_t1 == 0`, что может привести к делению на ноль. +6. **Точность измерения батареи** — зависит от стабильности внутреннего опорного напряжения 1,1 В. +7. **Программные задержки в TWI** — обмен с BME280 выполняется на аппаратном TWI, но ожидание `TWINT` реализовано поллингом с таймаутом; прерывания TWI не используются. +8. **Задержка между измерениями ~5 минут** — определяется произведением `WDT_WAKE_COUNT` (38) на интервал WDT (~8 секунд). В течение этого времени пакет не передаётся. +9. **Давление в мм рт. ст.** — `zh_bme280_compensate_pressure()` возвращает давление в Паскалях, для перевода в пакет умножается на `0.00750062` (коэффициент пересчёта Па → мм рт. ст.). +10. **Температура и влажность без масштабирования** — значения передаются как целые числа (°C и %), дробная часть теряется. + +--- + +## Требования для сборки + +- PlatformIO с плагином `platform-atmelavr` +- AVR-GCC (входит в состав PlatformIO) +- Библиотека `zh_rf_config` (подключается через `lib_deps` в `platformio.ini`) +- Программатор USBasp (или совместимый) +- Фьюзы: LFUSE=`0xE2`, HFUSE=`0xDF`, EFUSE=`0x07` (8 МГц, без делителя) + +--- + +## Установка + +### Сборка и прошивка через PlatformIO + +```bash +pio run -e ATmega328P -t upload +``` + +### Подключение в проекте + +Компонент является самодостаточной прошивкой — не подключается как библиотека. Для интеграции в другой проект скопируйте `src/main.c`, добавьте зависимость `zh_rf_config` и адаптируйте макросы конфигурации. + +```ini +; platformio.ini +lib_deps = + https://github.com/aZholtikov/zh_rf_config +``` + +```c +// Зависимости: стандартные заголовки AVR-libc, математика и конфигурация протокола +#include "avr/io.h" +#include "avr/sleep.h" +#include "avr/fuse.h" +#include "avr/interrupt.h" +#include "util/delay.h" +#include "math.h" +#include "zh_rf_config.h" +```