diff --git a/README.md b/README.md index d32bf00..7c2007f 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,19 @@ 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). +3. Automatic over-the-air pairing: the sensor requests a device id from the gateway and stores it in EEPROM. +4. Transmits device identifier, battery voltage, and all three BME280 sensor readings (×10 scaling) on each measurement cycle. +5. Battery measurement through internal ADC with 1.1 V bandgap reference. +6. Wake-up from Power-down mode by watchdog timer interrupt with configurable interval. +7. Supports ATMega168, ATMega168P, ATMega168PB, ATMega328, ATMega328P, and ATMega328PB. +8. 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). +1. A compatible RF Gateway is required to receive packets and assign a device id during pairing. +2. The watchdog timer interval is fixed at 8 seconds; the sensor transmits every 38 ticks (~5 minutes). +3. Changing gateway address or channel requires reflashing the microcontroller. +4. If the gateway does not respond during pairing, the device enters permanent sleep until reset. ## Using diff --git a/platformio.ini b/platformio.ini index 3a0c571..9600872 100644 --- a/platformio.ini +++ b/platformio.ini @@ -12,7 +12,7 @@ upload_flags = -Pusb -e lib_deps = - https://github.com/aZholtikov/zh_rf_config + https://github.com/aZholtikov/zh_rf_config#dev [env:ATmega168P] platform = atmelavr @@ -28,7 +28,7 @@ upload_flags = -Pusb -e lib_deps = - https://github.com/aZholtikov/zh_rf_config + https://github.com/aZholtikov/zh_rf_config#dev [env:ATmega168PB] platform = atmelavr @@ -44,7 +44,7 @@ upload_flags = -Pusb -e lib_deps = - https://github.com/aZholtikov/zh_rf_config + https://github.com/aZholtikov/zh_rf_config#dev [env:ATmega328] platform = atmelavr @@ -60,7 +60,7 @@ upload_flags = -Pusb -e lib_deps = - https://github.com/aZholtikov/zh_rf_config + https://github.com/aZholtikov/zh_rf_config#dev [env:ATmega328P] platform = atmelavr @@ -76,7 +76,7 @@ upload_flags = -Pusb -e lib_deps = - https://github.com/aZholtikov/zh_rf_config + https://github.com/aZholtikov/zh_rf_config#dev [env:ATmega328PB] platform = atmelavr @@ -92,4 +92,4 @@ upload_flags = -Pusb -e lib_deps = - https://github.com/aZholtikov/zh_rf_config \ No newline at end of file + https://github.com/aZholtikov/zh_rf_config#dev \ No newline at end of file diff --git a/src/main.c b/src/main.c index 97d4bda..a184214 100644 --- a/src/main.c +++ b/src/main.c @@ -1,6 +1,7 @@ #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" @@ -41,19 +42,40 @@ #ifndef TWDR #define TWDR TWDR0 #endif +#ifndef PRR +#define PRR PRR0 +#endif +#ifndef PRTWI +#define PRTWI PRTWI0 +#endif +#ifndef PRSPI +#define PRSPI PRSPI0 +#endif #endif #define GATEWAY_CHANNEL 120 #define GATEWAY_ADDRESS 0xDDEEFF -#define DEVICE_ID 6 + +#define EEPROM_DEVICE_ID_ADDR ((uint8_t *)0x00) +#define DEVICE_ID_UNASSIGNED 0xFF + +#define PAIRING_TIMEOUT_ATTEMPTS 7 #define WDT_WAKE_COUNT 38 -#define ZH_TWI_TWBR 32 +#define WDT_T_250MS 0x04 +#define WDT_T_4S 0x08 +#define WDT_T_8S 0x09 + +#define TWI_TWBR 32 #define NRF_CMD_W_REGISTER 0x20 #define NRF_CMD_W_TX_PAYLOAD 0xA0 +#define NRF_CMD_R_RX_PAYLOAD 0x61 #define NRF_CMD_FLUSH_TX 0xE1 +#define NRF_CMD_FLUSH_RX 0xE2 #define NRF_CMD_NOP 0xFF +#define NRF_CMD_ACTIVATE 0x50 +#define NRF_CMD_R_RX_PL_WID 0x60 #define NRF_REG_CONFIG 0x00 #define NRF_REG_EN_AA 0x01 @@ -65,15 +87,24 @@ #define NRF_REG_STATUS 0x07 #define NRF_REG_RX_ADDR_P0 0x0A #define NRF_REG_TX_ADDR 0x10 +#define NRF_REG_RX_PW_P0 0x11 +#define NRF_REG_DYNPD 0x1C +#define NRF_REG_FEATURE 0x1D #define NRF_CONFIG_MASK_RX_DR 0x40 +#define NRF_CONFIG_MASK_TX_DS 0x20 +#define NRF_CONFIG_MASK_MAX_RT 0x10 #define NRF_CONFIG_EN_CRC 0x08 #define NRF_CONFIG_PWR_UP 0x02 +#define NRF_CONFIG_PRIM_RX 0x01 #define NRF_STATUS_RX_DR 0x40 #define NRF_STATUS_TX_DS 0x20 #define NRF_STATUS_MAX_RT 0x10 +#define NRF_FEATURE_EN_DPL 0x04 +#define NRF_DYNPD_DPL_P0 0x01 + #define BME280_ADDR 0x76 #define BME280_REG_ID 0xD0 #define BME280_REG_RESET 0xE0 @@ -85,14 +116,40 @@ #define BME280_REG_CALIB26 0xE1 /** - * @brief Initialize the watchdog timer for periodic wake-up. + * @brief Load the device identifier from EEPROM. * - * 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. + * @return Stored device id, or DEVICE_ID_UNASSIGNED if none is stored */ -static void zh_wdt_init(void); +static uint8_t zh_load_device_id(void); + +/** + * @brief Save the device identifier to EEPROM. + * + * @param id Device identifier to store + */ +static void zh_save_device_id(uint8_t id); + +/** + * @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/16 + * 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 hardware TWI (I2C) peripheral. @@ -168,6 +225,18 @@ static uint8_t zh_i2c_read_ack(void); */ static uint8_t zh_i2c_read_nack(void); +/** + * @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 Write a register value to the BME280 sensor over TWI. * @@ -199,18 +268,6 @@ static bool zh_bme280_write_reg(uint8_t reg, uint8_t value); */ 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. * @@ -264,28 +321,6 @@ static float zh_bme280_compensate_pressure(int32_t adc_p); */ 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. * @@ -295,6 +330,15 @@ static uint8_t zh_spi_transfer(uint8_t data); */ static void zh_nrf24_init(void); +/** + * @brief Initialize INT0 external interrupt for the nRF24 IRQ signal. + * + * Configures PD2 as a falling-edge triggered external interrupt source and + * enables the INT0 interrupt mask bit. The ISR sets the nrf_event flag used + * by the pairing state machine. + */ +static void zh_nrf24_interrupt_init(void); + /** * @brief Transmit a packet via the nRF24L01 module. * @@ -308,6 +352,29 @@ static void zh_nrf24_init(void); */ static void zh_nrf24_send(uint8_t *buf, uint8_t size); +/** + * @brief Start a transmission without blocking on completion. + * + * Loads the payload, pulses CE for 15 us and returns immediately. The + * transmission completion is signalled by the nRF24 IRQ pin. + * + * @param buf Pointer to the payload data to transmit (must not be NULL) + * @param size Number of bytes in the payload buffer (must be > 0) + */ +static void zh_nrf24_send_no_wait(uint8_t *buf, uint8_t size); + +/** + * @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 Write a payload to the nRF24L01 using a write command. * @@ -350,16 +417,67 @@ static uint8_t zh_nrf24_write_register(uint8_t reg, uint8_t val); 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. + * @brief Read the received payload from the nRF24L01+ into a buffer. * - * 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 + * @param buf Pointer to the destination buffer (must not be NULL) + * @param count Number of bytes to read from the RX FIFO */ -static uint8_t zh_nrf24_send_command(uint8_t cmd); +static void zh_nrf24_read_payload(uint8_t *buf, uint8_t count); + +/** + * @brief Read the width of the payload in the nRF24L01+ RX FIFO. + * + * @return Payload width in bytes + */ +static uint8_t zh_nrf24_read_payload_width(void); + +/** + * @brief Switch the nRF24L01+ to RX mode on pipe 0. + */ +static void zh_nrf24_enter_rx(void); + +/** + * @brief Put the nRF24L01+ into power-down mode. + */ +static void zh_nrf24_enter_power_down(void); + +/** + * @brief Start the watchdog timer in interrupt mode with the given timeout. + * + * @param timeout_bits Watchdog timeout prescaler bits (WDP3:0) + */ +static void zh_wdt_start(uint8_t timeout_bits); + +/** + * @brief Stop the watchdog timer. + */ +static void zh_wdt_stop(void); + +/** + * @brief Generate a pseudo-random token from MCU state and ADC noise. + * + * @return 16-bit random token (never zero) + */ +static uint16_t zh_get_random_token(void); + +/** + * @brief Run the non-blocking pairing procedure to obtain a device id. + * + * Sends pairing requests, waits for the gateway acknowledgment in RX mode + * and retries with watchdog-timed backoff. Sleeps forever if the gateway + * does not answer within the allowed number of attempts. + * + * @return Device identifier assigned by the gateway + */ +static uint8_t zh_do_pairing(void); + +/** + * @brief Enter permanent power-down sleep until the next reset. + * + * Disables all interrupts, the watchdog, the radio, the ADC, SPI and all + * unused peripheral clocks, then loops in power-down sleep mode. + */ +static void zh_enter_final_sleep(void); /** * @brief Measure the battery voltage using the AVR ADC. @@ -373,22 +491,39 @@ static uint8_t zh_nrf24_send_command(uint8_t cmd); static float zh_get_battery_voltage(void); static volatile uint8_t wdt_wake_count = 0; +static volatile uint8_t nrf_event = false; 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}; +zh_rf_sensor_transmitted_data_t transmitted_data = {0}; 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(); - zh_wdt_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; + { + uint8_t probe[ZH_RF_MAX_PAYLOAD_SIZE]; + if (zh_rf_sensor_build_payload(&transmitted_data, probe, sizeof(probe)) == 0) + { + zh_enter_final_sleep(); + } + } + zh_wdt_start(WDT_T_8S); int32_t adc_t = 0; int32_t adc_p = 0; int32_t adc_h = 0; @@ -396,11 +531,16 @@ int main(void) { 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)); + 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 { @@ -423,11 +563,16 @@ int main(void) { 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.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)); - zh_nrf24_send((uint8_t *)&transmitted_data, sizeof(transmitted_data)); + 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 { @@ -439,20 +584,49 @@ int main(void) 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; + if (wdt_wake_count < WDT_WAKE_COUNT) + { + ++wdt_wake_count; + } +} + +ISR(INT0_vect) +{ + nrf_event = true; +} + +static uint8_t zh_load_device_id(void) +{ + uint8_t id = eeprom_read_byte(EEPROM_DEVICE_ID_ADDR); + if (id == 0xFF || id == 0x00) + { + return DEVICE_ID_UNASSIGNED; + } + return id; +} + +static void zh_save_device_id(uint8_t id) +{ + eeprom_update_byte(EEPROM_DEVICE_ID_ADDR, id); +} + +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_i2c_init(void) @@ -460,14 +634,14 @@ static void zh_i2c_init(void) DDRC &= ~((1 << PC4) | (1 << PC5)); PORTC |= (1 << PC4) | (1 << PC5); TWSR = 0x00; - TWBR = ZH_TWI_TWBR; + TWBR = TWI_TWBR; TWCR = (1 << TWEN); } static bool zh_i2c_wait(void) { uint16_t timeout = 65535; - while (!(TWCR & (1 << TWINT))) + while ((TWCR & (1 << TWINT)) == 0) { if (--timeout == 0) { @@ -492,7 +666,7 @@ static void zh_i2c_stop(void) { TWCR = (1 << TWINT) | (1 << TWSTO) | (1 << TWEN); uint16_t timeout = 1000; - while ((TWCR & (1 << TWSTO)) && --timeout) + while ((TWCR & (1 << TWSTO)) != 0 && --timeout) { } } @@ -529,6 +703,38 @@ static uint8_t zh_i2c_read_nack(void) return TWDR; } +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_write_reg(uint8_t reg, uint8_t value) { if (zh_i2c_start() == false) @@ -591,38 +797,6 @@ static bool zh_bme280_read_reg(uint8_t reg, uint8_t *buf, uint8_t len) 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) @@ -709,22 +883,6 @@ static float zh_bme280_compensate_humidity(int32_t adc_h) 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); @@ -733,6 +891,7 @@ static void zh_nrf24_init(void) DDRD &= ~(1 << PD2); PORTD |= (1 << PD2); uint64_t gateway_address = GATEWAY_ADDRESS; + uint8_t activation_key = 0x73; 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); @@ -743,9 +902,22 @@ static void zh_nrf24_init(void) 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_buffer(NRF_CMD_ACTIVATE, &activation_key, sizeof(activation_key)); + zh_nrf24_write_register(NRF_REG_FEATURE, NRF_FEATURE_EN_DPL); + zh_nrf24_write_register(NRF_REG_DYNPD, NRF_DYNPD_DPL_P0); zh_nrf24_write_register(NRF_REG_CONFIG, NRF_CONFIG_MASK_RX_DR | NRF_CONFIG_EN_CRC); } +static void zh_nrf24_interrupt_init(void) +{ + DDRD &= ~(1 << PD2); + PORTD |= (1 << PD2); + EICRA &= ~((1 << ISC01) | (1 << ISC00)); + EICRA |= (1 << ISC01); + EIFR |= (1 << INTF0); + EIMSK |= (1 << INT0); +} + 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); @@ -769,6 +941,27 @@ 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); } +static void zh_nrf24_send_no_wait(uint8_t *buf, uint8_t size) +{ + PORTB &= ~(1 << PB1); + zh_nrf24_write_register(NRF_REG_CONFIG, NRF_CONFIG_MASK_RX_DR | NRF_CONFIG_EN_CRC | NRF_CONFIG_PWR_UP); + _delay_ms(2); + zh_nrf24_send_command(NRF_CMD_FLUSH_TX); + zh_nrf24_write_register(NRF_REG_STATUS, NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT | NRF_STATUS_RX_DR); + zh_nrf24_write_buffer(NRF_CMD_W_TX_PAYLOAD, buf, size); + PORTB |= (1 << PB1); + _delay_us(15); + PORTB &= ~(1 << PB1); +} + +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 uint8_t zh_nrf24_write_buffer(uint8_t cmd, uint8_t *buf, uint8_t count) { PORTB &= ~(1 << PB2); @@ -795,12 +988,240 @@ static uint8_t zh_nrf24_write_buff_register(uint8_t reg, uint8_t *buf, uint8_t c return zh_nrf24_write_buffer((reg | NRF_CMD_W_REGISTER), buf, count); } -static uint8_t zh_nrf24_send_command(uint8_t cmd) +static void zh_nrf24_read_payload(uint8_t *buf, uint8_t count) { PORTB &= ~(1 << PB2); - uint8_t status = zh_spi_transfer(cmd); + zh_spi_transfer(NRF_CMD_R_RX_PAYLOAD); + while (count-- != 0) + { + *buf++ = zh_spi_transfer(0xFF); + } PORTB |= (1 << PB2); - return status; +} + +static uint8_t zh_nrf24_read_payload_width(void) +{ + PORTB &= ~(1 << PB2); + zh_spi_transfer(NRF_CMD_R_RX_PL_WID); + uint8_t width = zh_spi_transfer(0xFF); + PORTB |= (1 << PB2); + return width; +} + +static void zh_nrf24_enter_rx(void) +{ + PORTB &= ~(1 << PB1); + zh_nrf24_write_register(NRF_REG_STATUS, NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT | NRF_STATUS_RX_DR); + zh_nrf24_send_command(NRF_CMD_FLUSH_RX); + zh_nrf24_write_register(NRF_REG_CONFIG, NRF_CONFIG_EN_CRC | NRF_CONFIG_PWR_UP | NRF_CONFIG_PRIM_RX); + _delay_ms(2); + PORTB |= (1 << PB1); + _delay_us(130); +} + +static void zh_nrf24_enter_power_down(void) +{ + PORTB &= ~(1 << PB1); + zh_nrf24_write_register(NRF_REG_CONFIG, NRF_CONFIG_EN_CRC); +} + +static void zh_wdt_start(uint8_t timeout_bits) +{ + uint8_t wdp = (timeout_bits & 0x07) | ((timeout_bits & 0x08) ? (1 << WDP3) : 0); + cli(); + wdt_reset(); + WDTCSR = (1 << WDCE) | (1 << WDE); + WDTCSR = (1 << WDIE) | wdp; + sei(); +} + +static void zh_wdt_stop(void) +{ + cli(); + wdt_reset(); + WDTCSR = (1 << WDCE) | (1 << WDE); + WDTCSR = 0; + sei(); +} + +static uint16_t zh_get_random_token(void) +{ + uint16_t token = MCUSR; + MCUSR = 0; + ADMUX = (1 << REFS0); + ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0); + _delay_ms(5); + for (uint8_t i = 0; i < 16; ++i) + { + ADCSRA |= (1 << ADSC); + while ((ADCSRA & (1 << ADSC)) != 0) + { + } + token = (token << 1) ^ (token >> 15) ^ ADC; + } + ADCSRA &= ~(1 << ADEN); + if (token == 0) + { + token = 0xACE1; + } + return token; +} + +static uint8_t zh_do_pairing(void) +{ + uint16_t token = zh_get_random_token(); + zh_rf_sensor_pairing_data_t pairing_request; + pairing_request.type = (uint8_t)transmitted_data.type; + pairing_request.magic = ZH_RF_PAIRING_REQ_MAGIC; + pairing_request.token_hi = (uint8_t)(token >> 8); + pairing_request.token_lo = (uint8_t)(token & 0xFF); + pairing_request.id = 0; + zh_nrf24_interrupt_init(); + zh_rf_sensor_pairing_state_t state = ZH_RF_PAIR_SEND; + uint8_t attempts = 0; + for (;;) + { + switch (state) + { + case ZH_RF_PAIR_SEND: + nrf_event = false; + zh_nrf24_send_no_wait((uint8_t *)&pairing_request, ZH_RF_PAIRING_PACKET_SIZE); + state = ZH_RF_PAIR_TX_WAIT; + break; + case ZH_RF_PAIR_TX_WAIT: + if ((PIND & (1 << PD2)) == 0) + { + nrf_event = true; + } + else + { + nrf_event = false; + zh_wdt_start(WDT_T_250MS); + sleep_enable(); + sei(); + sleep_cpu(); + sleep_disable(); + zh_wdt_stop(); + sei(); + } + if (nrf_event == true) + { + nrf_event = false; + uint8_t status = zh_nrf24_send_command(NRF_CMD_NOP); + zh_nrf24_write_register(NRF_REG_STATUS, NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT | NRF_STATUS_RX_DR); + if ((status & NRF_STATUS_TX_DS) != 0) + { + zh_nrf24_enter_rx(); + state = ZH_RF_PAIR_RX_WAIT; + } + else + { + state = ZH_RF_PAIR_BACKOFF; + } + } + else + { + state = ZH_RF_PAIR_BACKOFF; + } + break; + case ZH_RF_PAIR_RX_WAIT: + if ((PIND & (1 << PD2)) == 0) + { + nrf_event = true; + } + else + { + nrf_event = false; + zh_wdt_start(WDT_T_250MS); + sleep_enable(); + sei(); + sleep_cpu(); + sleep_disable(); + zh_wdt_stop(); + sei(); + } + if (nrf_event == true) + { + nrf_event = false; + uint8_t status = zh_nrf24_send_command(NRF_CMD_NOP); + if ((status & NRF_STATUS_RX_DR) != 0) + { + uint8_t width = zh_nrf24_read_payload_width(); + if (width > ZH_RF_MAX_PAYLOAD_SIZE) + { + zh_nrf24_send_command(NRF_CMD_FLUSH_RX); + zh_nrf24_write_register(NRF_REG_STATUS, NRF_STATUS_RX_DR); + state = ZH_RF_PAIR_BACKOFF; + break; + } + if (width != ZH_RF_PAIRING_PACKET_SIZE) + { + uint8_t dummy[ZH_RF_MAX_PAYLOAD_SIZE]; + zh_nrf24_read_payload(dummy, width); + zh_nrf24_write_register(NRF_REG_STATUS, NRF_STATUS_RX_DR); + zh_nrf24_send_command(NRF_CMD_FLUSH_RX); + state = ZH_RF_PAIR_BACKOFF; + break; + } + zh_rf_sensor_pairing_data_t pairing_response; + zh_nrf24_read_payload((uint8_t *)&pairing_response, width); + zh_nrf24_write_register(NRF_REG_STATUS, NRF_STATUS_RX_DR | NRF_STATUS_TX_DS | NRF_STATUS_MAX_RT); + zh_nrf24_send_command(NRF_CMD_FLUSH_RX); + if (pairing_response.magic == ZH_RF_PAIRING_ACK_MAGIC && pairing_response.type == pairing_request.type && + pairing_response.token_hi == pairing_request.token_hi && pairing_response.token_lo == pairing_request.token_lo && + pairing_response.id != 0 && pairing_response.id != DEVICE_ID_UNASSIGNED) + { + zh_nrf24_enter_power_down(); + EIMSK &= ~(1 << INT0); + zh_save_device_id(pairing_response.id); + return pairing_response.id; + } + } + state = ZH_RF_PAIR_BACKOFF; + } + else + { + state = ZH_RF_PAIR_BACKOFF; + } + break; + case ZH_RF_PAIR_BACKOFF: + zh_nrf24_enter_power_down(); + nrf_event = false; + zh_wdt_start(WDT_T_4S); + sleep_enable(); + sei(); + sleep_cpu(); + sleep_disable(); + zh_wdt_stop(); + sei(); + if (++attempts >= PAIRING_TIMEOUT_ATTEMPTS) + { + zh_enter_final_sleep(); + } + state = ZH_RF_PAIR_SEND; + break; + } + } +} + +static void zh_enter_final_sleep(void) +{ + cli(); + EIMSK = 0; + EIFR = 0xFF; + zh_wdt_stop(); + PORTB &= ~(1 << PB1); + zh_nrf24_write_register(NRF_REG_CONFIG, 0x00); + ADCSRA = 0; + TWCR = 0; + SPCR = 0; + PRR = (1 << PRTWI) | (1 << PRTIM2) | (1 << PRTIM1) | (1 << PRTIM0) | (1 << PRSPI) | (1 << PRUSART0); + set_sleep_mode(SLEEP_MODE_PWR_DOWN); + sleep_enable(); + for (;;) + { + sleep_cpu(); + } } static float zh_get_battery_voltage(void) @@ -809,11 +1230,11 @@ static float zh_get_battery_voltage(void) ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0); _delay_ms(10); ADCSRA |= (1 << ADSC); - while (ADCSRA & (1 << ADSC)) + while ((ADCSRA & (1 << ADSC)) != 0) { } ADCSRA |= (1 << ADSC); - while (ADCSRA & (1 << ADSC)) + while ((ADCSRA & (1 << ADSC)) != 0) { } uint16_t adc = ADCL; @@ -824,4 +1245,4 @@ static float zh_get_battery_voltage(void) 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 index 3623570..551708f 100644 --- a/wiki_ru.md +++ b/wiki_ru.md @@ -4,17 +4,18 @@ **zh_avr_bme280_sensor** — прошивка для AVR-микроконтроллеров ATMega168/328, реализующая беспроводной датчик климата на базе датчика BME280 и трансивера nRF24L01+. -Компонент работает в режиме глубокого сна (Power-down), просыпается по таймеру (watchdog), измеряет температуру, влажность и давление на датчике BME280 (интерфейс I2C) и передаёт на шлюз пакет с идентификатором устройства и уровнем заряда батареи. +Компонент работает в режиме глубокого сна (Power-down), просыпается по таймеру (watchdog), измеряет температуру, влажность и давление на датчике BME280 (интерфейс I2C) и передаёт на шлюз пакет с идентификатором устройства и уровнем заряда батареи. Идентификатор устройства получается автоматически по воздуху в процедуре сопряжения со шлюзом и сохраняется в EEPROM. ### Основные возможности 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. **Автономная работа** — не требует внешних подтягивающих резисторов. +3. **Автоматическое сопряжение (pairing)** — запрос идентификатора устройства у шлюза по воздуху, хранение ID в EEPROM. +4. **Передача уровня заряда батареи** — измерение напряжения через внутренний АЦП с опорным напряжением 1,1 В. +5. **Работа по протоколу nRF24L01+** — передача и приём через SPI, адресная доставка на шлюз, динамическая длина пакета. +6. **Поддержка нескольких МК** — ATMega168/168P/168PB/328/328P/328PB. +7. **Пробуждение по watchdog-таймеру** — WDT с интервалом ~8 секунд, цикл работы каждые `WDT_WAKE_COUNT` (38) прерываний ≈ 5 минут. +8. **Автономная работа** — не требует внешних подтягивающих резисторов (используются внутренние pull-up). --- @@ -22,38 +23,51 @@ ### Структура данных -Компонент не использует динамическое выделение памяти и сложные структуры данных. Все данные передаются как массив `int16_t` фиксированной длины. +Компонент не использует динамическое выделение памяти. Сырые показания хранятся в статических переменных, калибровочные коэффициенты 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` и передаётся побайтово как массив байтов (`sizeof` структуры — 14 байт). +Данные формируются в глобальной переменной `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` | Идентификатор устройства (`DEVICE_ID = 6`) | +| `id` | `int16_t` | Идентификатор устройства, назначенный шлюзом при сопряжении | | `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) | +| `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-байтовый адрес шлюза | -| `DEVICE_ID` | `6` | Уникальный идентификатор устройства | -| `WDT_WAKE_COUNT` | `38` | Количество прерываний WDT до пробуждения (≈ 5 минут) | -| `ZH_TWI_TWBR` | `32` | Значение регистра TWBR для задания скорости аппаратного TWI | +| `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` объявлена как `static` и заполняется при старте (идентификатор и тип). В цикле обновляются все поля данных: `battery_voltage`, `value_1` (влажность), `value_2` (температура), `value_3` (давление). +Компонент не использует динамическую память. Все данные хранятся в статической области и регистрах микроконтроллера. Глобальная переменная `transmitted_data` заполняется при старте (тип устройства, затем идентификатор из EEPROM или из сопряжения). В цикле обновляются поля `battery`, `value_1` (влажность), `value_2` (температура), `value_3` (давление). Идентификатор устройства сохраняется в EEPROM адресом `0x00` (`eeprom_update_byte`). --- @@ -65,11 +79,23 @@ | Функция | Описание | | --------- | ---------- | -| `zh_wdt_init()` | Инициализация watchdog-таймера (~8 сек, прерывание WDT_vect) для периодического пробуждения | +| `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/2) | -| `zh_nrf24_init()` | Инициализация трансивера nRF24L01+ в режим передачи | +| `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, радио, тактов периферии) | ### Измерение @@ -81,16 +107,19 @@ | Функция | Описание | | --------- | ---------- | -| `zh_bme280_read()` | Чтение 8 сырых байт из BME280 и распаковка ADC значений температуры, давления, влажности | +| `zh_bme280_read()` | Перевод BME280 в forced mode, чтение 8 сырых байт и распаковка ADC значений температуры, давления, влажности | | `zh_bme280_compensate_temperature()` | Компенсация сырой температуры по калибровочным коэффициентам (результат в °C) | | `zh_bme280_compensate_pressure()` | Компенсация сырого давления по калибровочным коэффициентам (результат в Па) | -| `zh_bme280_compensate_humidity()` | Компенсация сырой влажности по калибровочным коэффициентам (результат в %, диапазон [0, 100]) | +| `zh_bme280_compensate_humidity()` | Компенсация сырой влажности по калибровочным коэффициентам (результат в %) | -### Передача данных +### Передача данных nRF24L01+ | Функция | Описание | -|---------|----------| -| `zh_nrf24_send()` | Передача буфера через nRF24L01+: Power Up, `FLUSH_TX`, `W_TX_PAYLOAD`, импульс CE, ожидание IRQ с таймаутом 100 мс, сброс статуса | +| --------- | ---------- | +| `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-операции @@ -101,6 +130,8 @@ | `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) @@ -124,33 +155,50 @@ ## Потокобезопасность -Компонент работает на bare-metal AVR без RTOS. Единственная точка синхронизации — ISR watchdog-таймера (`ISR(WDT_vect)`), которая увеличивает `wdt_wake_count` (объявлен как `static volatile uint8_t`). Главный цикл проверяет этот счётчик после выхода из сна. Атомарная запись одного байта на AVR гарантирует отсутствие состояний гонки при инкременте. Весь остальной обмен с периферией (I2C, SPI, АЦП) выполняется последовательно в главном цикле. +Компонент работает на 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` (magic `0xA5`) без ожидания (`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`. +- Сброс записью `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). +- Запуск измерения: `ctrl_meas = 0x25` (temp/pressure oversampling x16, forced mode) в `zh_bme280_read()` перед каждым чтением. ### Алгоритм компенсации BME280 Сырые ADC-значения пересчитываются в физические величины по алгоритмам производителя из datasheet BME280. -- **Температура**: `zh_bme280_compensate_temperature()` — вычисляет `bme280_t_fine` (используется для давления и влажности), возвращает результат в °C. Деление на `dig_t1` не выполняется (проверка `dig_t1 == 0` не реализована). +- **Температура**: `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 = ZH_TWI_TWBR` (32), включение модуля битом `TWEN`. Скорость: `F_CPU / (16 + 2·TWBR·prescaler)` ≈ 100 кГц при 8 МГц. +- Инициализация: `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`. @@ -162,18 +210,21 @@ - Переход в Power Up (бит `PWR_UP` в CONFIG). - Ожидание 5 мс (время стартования осциллятора). - Очистка TX FIFO командой `FLUSH_TX`. -- Загрузка полезной нагрузки командой `W_TX_PAYLOAD`. -- Импульс CE длительностью 20 мкс для запуска передачи. -- Ожидание завершения по пину IRQ (PD2) с таймаутом 100 мс. +- Загрузка полезной нагрузки командой `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 секунд (`WDP3 | WDP0`), без сброса MCU. -- ISR `WDT_vect`: увеличивает `wdt_wake_count`, заново включает бит `WDIE`. +- Инициализация 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+. -- Первое измерение выполняется сразу после инициализации перед входом в цикл. +- При достижении порога: обнуление счётчика, повторная инициализация BME280 (если датчик недоступен), чтение показаний, сериализация пакета и передача через nRF24L01+. +- Первое измерение выполняется сразу после инициализации и сопряжения, до входа в цикл. +- `zh_enter_final_sleep()` — аварийный постоянный сон: отключение прерываний, WDT, радио, АЦП, TWI, SPI и тактов всей неиспользуемой периферии (PRR). --- @@ -184,30 +235,43 @@ ```c 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(); - zh_wdt_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_voltage = + 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)); + (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); + (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)); - zh_nrf24_send((uint8_t *)&transmitted_data, - sizeof(transmitted_data)); + (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; } @@ -225,7 +289,7 @@ int main(void) } if (bme280_present == true) { if (zh_bme280_read(&adc_t, &adc_p, &adc_h) == true) { - transmitted_data.battery_voltage = + 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)); @@ -233,8 +297,12 @@ int main(void) (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)); + 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; } @@ -247,8 +315,9 @@ int main(void) ### Изменение конфигурации под другое устройство ```c -// Измените идентификатор для нового устройства -#define DEVICE_ID 7 // Уникальный ID в сети +// Идентификатор устройства назначается шлюзом при сопряжении +// и хранится в EEPROM (адрес 0x00). Для повторного сопряжения +// обнулите ячейку EEPROM. #define GATEWAY_CHANNEL 120 #define GATEWAY_ADDRESS 0xDDEEFF // Тип устройства задаётся перечислением ZH_RF_SENSOR_BME280 @@ -263,10 +332,10 @@ int main(void) // PB3 -> MOSI (назначается аппаратным SPI) // PB5 -> SCK (назначается аппаратным SPI) // PB1 -> CE (управление передачей, выход) -// PD2 -> IRQ (вход, активный низкий, с подтяжкой к VCC) +// PD2 -> IRQ (вход, активный низкий, INT0, с подтяжкой к VCC) // BME280 (I2C, аппаратный TWI): -// PC4 -> SDA (аппаратный TWI, pull-up) -// PC5 -> SCL (аппаратный TWI, pull-up) +// PC4 -> SDA (аппаратный TWI, внутренний pull-up) +// PC5 -> SCL (аппаратный TWI, внутренний pull-up) ``` --- @@ -279,7 +348,7 @@ int main(void) ## Ошибки -Компонент не возвращает коды ошибок. Функции `zh_bme280_init`, `zh_bme280_read`, `zh_bme280_write_reg`, `zh_bme280_read_reg` возвращают `bool` (true при успехе). Остальные функции имеют тип `void` или возвращают однобайтовый статус SPI (`uint8_t`). +Компонент не возвращает коды ошибок. Функции `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`). | Условие | Поведение | | --------- | ---------- | @@ -291,21 +360,24 @@ int main(void) | Ошибка чтения 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 | Неизвестный тип датчика или слишком мал буфер — переход в постоянный сон | --- ## Ограничения -1. **Только передача (TX-only)** — компонент не принимает данные, шлюз не может отправить подтверждение на уровне приложения. +1. **Широковещательная передача данных** — приём используется только на этапе сопряжения; шлюз не подтверждает пакеты измерений на уровне приложения. 2. **Отсутствие шифрования** — CRC включён, но AES-шифрование не используется. Пакеты могут быть перехвачены. -3. **Жёстко зашитый адрес шлюза** — смена адреса требует перепрошивки. +3. **Жёстко зашитый адрес шлюза** — смена адреса или канала требует перепрошивки (ID устройства при этом хранится в EEPROM). 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 и %), дробная часть теряется. +5. **Точность измерения батареи** — зависит от стабильности внутреннего опорного напряжения 1,1 В. +6. **Программные задержки в TWI** — обмен с BME280 выполняется на аппаратном TWI, но ожидание `TWINT` реализовано поллингом с таймаутом; прерывания TWI не используются. +7. **Задержка между измерениями ~5 минут** — определяется произведением `WDT_WAKE_COUNT` (38) на интервал WDT (~8 секунд). В течение этого времени пакет не передаётся. +8. **Давление в мм рт. ст.** — `zh_bme280_compensate_pressure()` возвращает давление в Паскалях, для перевода в пакет умножается на `0.00750062` (коэффициент пересчёта Па → мм рт. ст.). +9. **Масштабирование значений ×10 только в первом цикле** — при первом измерении температура, влажность и давление умножаются на 10 (одна дробная цифра); в цикле пробуждения значения записываются без масштабирования (см. `main`). +10. **Сопряжение требует шлюза** — без ответившего шлюза устройство после 7 попыток уходит в постоянный сон до сброса/перепрошивки. --- @@ -313,7 +385,7 @@ int main(void) - PlatformIO с плагином `platform-atmelavr` - AVR-GCC (входит в состав PlatformIO) -- Библиотека `zh_rf_config` (подключается через `lib_deps` в `platformio.ini`) +- Библиотека `zh_rf_config` (ветка `dev`, подключается через `lib_deps` в `platformio.ini`) - Программатор USBasp (или совместимый) - Фьюзы: LFUSE=`0xE2`, HFUSE=`0xDF`, EFUSE=`0x07` (8 МГц, без делителя) @@ -334,16 +406,18 @@ pio run -e ATmega328P -t upload ```ini ; platformio.ini lib_deps = - https://github.com/aZholtikov/zh_rf_config + https://github.com/aZholtikov/zh_rf_config#dev ``` ```c // Зависимости: стандартные заголовки AVR-libc, математика и конфигурация протокола #include "avr/io.h" -#include "avr/sleep.h" -#include "avr/fuse.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" ```