diff --git a/README.md b/README.md index e7a9636..3e3a748 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ ## Features -1. Wireless touch pad sensor for ATMega 168/328 microcontrollers with nRF24L01+ transceiver. +1. Wireless touch pad sensor for ATMega168/328 microcontrollers with nRF24L01+ transceiver. 2. Power consumption in the sleep mode approximately 2 uA. 3. Transmits device identifier and battery voltage level on each touch event. 4. Battery measurement through internal ADC with 1.1 V bandgap reference. 5. Wake-up from power-down mode by external interrupt on rising edge. -6. Supports ATMega168, ATMega168P, ATMega168PB, ATMega328, ATMega328P, ATMega328PB. -7. Easy installation into any standard touch pad enclosure. +6. Automatic pairing with the gateway to obtain a device identifier stored in EEPROM. +7. Supports ATMega168, ATMega168P, ATMega168PB, ATMega328, ATMega328P, ATMega328PB. ## Attention @@ -16,6 +16,7 @@ 2. After power-on reset the first touch event may not trigger a transmission. 3. Multi-touch is not supported, only a single touch event per transmission cycle. 4. Changing gateway address or channel requires reflashing the microcontroller. +5. 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 37367ad..8613fef 100644 --- a/src/main.c +++ b/src/main.c @@ -2,8 +2,11 @@ #include "avr/sleep.h" #include "avr/fuse.h" #include "avr/interrupt.h" +#include "avr/eeprom.h" +#include "avr/wdt.h" #include "util/delay.h" #include "math.h" +#include "stdbool.h" #include "zh_rf_config.h" #if defined(__AVR_ATmega328PB__) @@ -28,16 +31,36 @@ #ifndef SPIF #define SPIF SPIF0 #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_T_250MS 0x04 +#define WDT_T_4S 0x08 #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 @@ -49,15 +72,38 @@ #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 + +/** + * @brief Load the device identifier from EEPROM. + * + * @return Stored device id, or DEVICE_ID_UNASSIGNED if none is stored + */ +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 INT1 external interrupt for the touch-pad wake-up signal. * @@ -93,6 +139,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 payload buffer through the nRF24L01+ module. * @@ -105,6 +160,26 @@ 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 single-byte command to the nRF24L01+ and read the status. + * + * @param cmd Command byte to send + * + * @return Status register value returned by the nRF24L01+ + */ +static uint8_t zh_nrf24_send_command(uint8_t cmd); + /** * @brief Write a data buffer to the nRF24L01+ via SPI with a given command. * @@ -144,13 +219,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 single-byte command to the nRF24L01+ and read the status. + * @brief Read the received payload from the nRF24L01+ into a buffer. * - * @param cmd Command byte to send - * - * @return Status register value returned by the nRF24L01+ + * @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 internal AVcc reference. @@ -163,32 +292,55 @@ static uint8_t zh_nrf24_send_command(uint8_t cmd); */ static float zh_get_battery_voltage(void); -static volatile uint8_t send_request = 0; +static volatile uint8_t send_request = false; +static volatile uint8_t nrf_event = false; +static volatile uint8_t wdt_event = false; -zh_rf_sensor_transmitted_data_t transmitted_data = {DEVICE_ID, ZH_RF_SENSOR_TOUCH_SWITCH, 0, 0, 0, 0, 0}; +zh_rf_sensor_transmitted_data_t transmitted_data = {0}; int main(void) { + transmitted_data.type = ZH_RF_SENSOR_TOUCH_SWITCH; + MCUSR &= ~(1 << WDRF); + wdt_disable(); cli(); zh_pad_interrupt_init(); zh_spi_init(); zh_nrf24_init(); set_sleep_mode(SLEEP_MODE_PWR_DOWN); sei(); + uint8_t device_id = zh_load_device_id(); + if (device_id == DEVICE_ID_UNASSIGNED) + { + device_id = zh_do_pairing(); + } + transmitted_data.id = (int16_t)device_id; + { + uint8_t probe[ZH_RF_MAX_PAYLOAD_SIZE]; + if (zh_rf_sensor_build_payload(&transmitted_data, probe, sizeof(probe)) == 0) + { + zh_enter_final_sleep(); + } + } for (;;) { - if (send_request == 1) + if (send_request == true) { cli(); - send_request = 0; + send_request = false; sei(); - transmitted_data.battery_voltage = (int16_t)lroundf(zh_get_battery_voltage() * 100.0); - zh_nrf24_send((uint8_t *)&transmitted_data, sizeof(transmitted_data)); + transmitted_data.battery = (int16_t)lroundf(zh_get_battery_voltage() * 100.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 { cli(); - if (send_request == 0) + if (send_request == false) { sleep_enable(); sei(); @@ -203,7 +355,32 @@ int main(void) ISR(INT1_vect) { - send_request = 1; + send_request = true; +} + +ISR(INT0_vect) +{ + nrf_event = true; +} + +ISR(WDT_vect) +{ + wdt_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_pad_interrupt_init(void) @@ -240,6 +417,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); @@ -250,9 +428,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); @@ -276,6 +467,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); @@ -302,12 +514,244 @@ 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--) + { + *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; + wdt_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; + wdt_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; + wdt_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; + wdt_event = false; + zh_wdt_start(WDT_T_4S); + sleep_enable(); + sei(); + sleep_cpu(); + sleep_disable(); + zh_wdt_stop(); + sei(); + wdt_event = false; + 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; + 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) @@ -316,11 +760,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; diff --git a/wiki_ru.md b/wiki_ru.md index f14ec12..41f2f83 100644 --- a/wiki_ru.md +++ b/wiki_ru.md @@ -1,53 +1,61 @@ -# Описание компонента zh_avr_touch_pad +# Описание прошивки zh_avr_touch_pad ## Общие сведения **zh_avr_touch_pad** — прошивка для AVR-микроконтроллеров ATMega168/328, реализующая беспроводную сенсорную кнопку на базе трансивера nRF24L01+. -Компонент работает в режиме глубокого сна (Power-down), пробуждается по внешнему прерыванию от сенсорного датчика и передаёт на шлюз пакет с идентификатором устройства и уровнем заряда батареи. +Прошивка работает в режиме глубокого сна (Power-down), пробуждается по внешнему прерыванию от сенсорного датчика и передаёт на шлюз пакет с идентификатором устройства и уровнем заряда батареи. При первом запуске выполняется автоматическое сопряжение (pairing) со шлюзом для получения идентификатора устройства, который сохраняется в EEPROM. ### Основные возможности 1. **Сверхнизкое энергопотребление** — около 2 мкА в режиме сна. 2. **Передача уровня заряда батареи** — измерение напряжения через внутренний АЦП с опорным напряжением 1,1 В. -3. **Работа по протоколу nRF24L01+** — передача через SPI, адресная доставка на шлюз. -4. **Поддержка нескольких МК** — ATMega168/168P/168PB/328/328P/328PB. -5. **Минимальная обвязка** — установка в стандартный корпус сенсорного выключателя. +3. **Автоматическое сопряжение со шлюзом** — получение идентификатора устройства по радиоканалу с сохранением в EEPROM. +4. **Работа по протоколу nRF24L01+** — передача через SPI, адресная доставка на шлюз. +5. **Поддержка нескольких МК** — ATMega168/168P/168PB/328/328P/328PB. 6. **Пробуждение по внешнему прерыванию INT1** — rising edge на пине PD3. -7. **Автономная работа** — не требует внешних подтягивающих резисторов. +7. **Генерация псевдослучайного токена** — на основе шума АЦП и состояния MCUSR для уникализации запроса сопряжения. +8. **Watchdog-таймер в режиме прерывания** — используется для таймаутов при сопряжении без сброса МК. --- ## Внутреннее устройство -### Структура данных +### Структура передаваемых данных -Компонент не использует динамическое выделение памяти и сложные структуры данных. Все данные передаются как массив `int16_t` фиксированной длины. +Прошивка не использует динамическое выделение памяти. Все данные хранятся в статической области и регистрах микроконтроллера. -#### Формат передаваемого пакета - -Пакет формируется в глобальной переменной `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_build_payload()` перед передачей. | Поле | Тип | Описание | | ------ | ----- | ---------- | -| `id` | `int16_t` | Идентификатор устройства (`DEVICE_ID = 6`) | +| `id` | `int16_t` | Идентификатор устройства, присвоенный шлюзом при сопряжении | | `type` | `int16_t` | Тип устройства (`ZH_RF_SENSOR_TOUCH_SWITCH`) | -| `battery_voltage` | `int16_t` | Напряжение батареи × 100 (например, 330 = 3.30 В) | -| `value_1`–`value_4` | `int16_t` | Зарезервированы (равны 0) | +| `battery` | `int16_t` | Напряжение батареи × 100 (например, 330 = 3.30 В) | ### Конфигурационные макросы | Макрос | Значение | Описание | -|--------|----------|----------| +| -------- | ---------- | ---------- | | `GATEWAY_CHANNEL` | `120` | Радиоканал шлюза | | `GATEWAY_ADDRESS` | `0xDDEEFF` | 3-байтовый адрес шлюза | -| `DEVICE_ID` | `6` | Уникальный идентификатор устройства | - -Тип устройства задаётся не макросом, а значением перечисления `ZH_RF_SENSOR_TOUCH_SWITCH` из библиотеки `zh_rf_config`. +| `EEPROM_DEVICE_ID_ADDR` | `((uint8_t *)0x00)` | Адрес в EEPROM для хранения идентификатора устройства | +| `DEVICE_ID_UNASSIGNED` | `0xFF` | Значение, обозначающее отсутствие сохранённого идентификатора | +| `PAIRING_TIMEOUT_ATTEMPTS` | `7` | Максимальное число попыток сопряжения перед уходом в окончательный сон | +| `WDT_T_250MS` | `0x04` | Таймаут watchdog-таймера 250 мс | +| `WDT_T_4S` | `0x08` | Таймаут watchdog-таймера 4 с | ### Управление памятью -Компонент не использует динамическую память. Все данные хранятся в статической области и регистрах микроконтроллера. Глобальная переменная `transmitted_data` заполняется один раз при старте (идентификатор и тип), в рабочем цикле обновляется только поле `battery_voltage`. Флаг `send_request` объявлен как `static volatile uint8_t` для корректного доступа из ISR и главного цикла. +Прошивка не использует динамическую память. Глобальная переменная `transmitted_data` заполняется при старте (идентификатор и тип), в рабочем цикле обновляется только поле `battery`. Флаги `send_request`, `nrf_event`, `wdt_event` объявлены как `static volatile uint8_t` для корректного доступа из ISR и главного цикла. + +### Флаги синхронизации + +| Флаг | Устанавливается | Используется | +| ------ | ----------------- | -------------- | +| `send_request` | ISR `INT1_vect` (касание) | Главный цикл — запуск передачи | +| `nrf_event` | ISR `INT0_vect` (IRQ nRF24) | Конечный автомат сопряжения | +| `wdt_event` | ISR `WDT_vect` (watchdog) | Конечный автомат сопряжения | --- @@ -62,68 +70,118 @@ | `zh_pad_interrupt_init()` | Настройка внешнего прерывания INT1 (PD3, rising edge) для пробуждения по касанию | | `zh_spi_init()` | Инициализация аппаратного SPI в режиме мастера (Fosc/16) | | `zh_nrf24_init()` | Инициализация трансивера nRF24L01+ в режим передачи | +| `zh_nrf24_interrupt_init()` | Настройка внешнего прерывания INT0 (PD2, falling edge) для сигнала IRQ nRF24 | + +### Работа с EEPROM + +| Функция | Описание | +|---------|----------| +| `zh_load_device_id()` | Чтение идентификатора устройства из EEPROM, возврат `DEVICE_ID_UNASSIGNED` если не сохранён | +| `zh_save_device_id()` | Сохранение идентификатора устройства в EEPROM | ### Измерение | Функция | Описание | |---------|----------| -| `zh_get_battery_voltage()` | Измерение напряжения батареи через внутренний АЦП с опорой 1,1 В | +| `zh_get_battery_voltage()` | Измерение напряжения батареи через внутренний АЦП с опорой 1,1 В, возврат значения в вольтах | +| `zh_get_random_token()` | Генерация 16-битного псевдослучайного токена из шума АЦП и MCUSR для запроса сопряжения | ### Передача данных | Функция | Описание | |---------|----------| -| `zh_nrf24_send()` | Передача буфера через nRF24L01+ с полным циклом TX: Power Up, `FLUSH_TX`, загрузка `W_TX_PAYLOAD`, импульс CE, ожидание флага TX_DS/MAX_RT по пину IRQ, сброс статуса и переход в Power Down | +| `zh_nrf24_send()` | Блокирующая передача буфера через nRF24L01+ с ожиданием завершения по пину IRQ (таймаут 100 мс) | +| `zh_nrf24_send_no_wait()` | Неблокирующая передача — загружает буфер, импульс CE 15 мкс, завершение по прерыванию IRQ | + +### Управление режимами nRF24L01+ + +| Функция | Описание | +|---------|----------| +| `zh_nrf24_enter_rx()` | Переключение nRF24L01+ в режим приёма на трубе 0 | +| `zh_nrf24_enter_power_down()` | Перевод nRF24L01+ в режим Power Down | ### Низкоуровневые SPI-операции | Функция | Описание | | --------- | ---------- | | `zh_spi_transfer()` | Обмен одного байта по SPI (полный дуплекс) | +| `zh_nrf24_send_command()` | Отправка однобайтовой команды и чтение регистра статуса | | `zh_nrf24_write_register()` | Запись одного байта в регистр nRF24L01+ | | `zh_nrf24_write_buff_register()` | Запись многобайтового буфера в регистр nRF24L01+ | -| `zh_nrf24_write_buffer()` | Запись буфера данных с указанной командой | -| `zh_nrf24_send_command()` | Отправка однобайтовой команды и чтение статуса | +| `zh_nrf24_write_buffer()` | Запись буфера данных с указанной SPI-командой | +| `zh_nrf24_read_payload()` | Чтение принятого пакета из RX FIFO nRF24 в буфер | +| `zh_nrf24_read_payload_width()` | Чтение длины пакета в RX FIFO nRF24 | + +### Watchdog-таймер + +| Функция | Описание | +|---------|----------| +| `zh_wdt_start()` | Запуск watchdog-таймера в режиме прерывания с указанным таймаутом | +| `zh_wdt_stop()` | Остановка watchdog-таймера | + +### Сон и сопряжение + +| Функция | Описание | +|---------|----------| +| `zh_do_pairing()` | Запуск конечного автомата сопряжения со шлюзом для получения идентификатора | +| `zh_enter_final_sleep()` | Переход в окончательный Power-down сон до сброса: отключение всех прерываний, watchdog, радиомодуля, АЦП, SPI и неиспользуемых периферийных блоков | --- ## Потокобезопасность -Компонент работает на bare-metal AVR без RTOS. Синхронизация между прерыванием и главным циклом реализуется следующим образом: +Прошивка работает на bare-metal AVR без RTOS. Синхронизация между прерываниями и главным циклом реализуется следующим образом: -1. **`volatile` флаг** — переменная `send_request` объявлена как `volatile`, что гарантирует чтение актуального значения из памяти при каждой проверке в главном цикле. -2. **Атомарная запись из ISR** — обработка INT1 устанавливает флаг в единицу. Запись одного байта на AVR является атомарной операцией. -3. **Отключение прерываний при инициализации** — `cli()` перед настройкой периферии исключает гонки при старте. +1. **Флаги `volatile`** — переменные `send_request`, `nrf_event`, `wdt_event` объявлены как `static volatile uint8_t`, что гарантирует чтение актуального значения из памяти при каждой проверке в главном цикле. +2. **Атомарная запись из ISR** — обработка прерывания устанавливает флаг в `true`. Запись одного байта на AVR является атомарной операцией. +3. **Отключение прерываний при проверке и сбросе флага** — `cli()` перед проверкой и сбросом `send_request` в главном цикле исключает гонку между ISR и главным циклом. +4. **Отключение прерываний при инициализации** — `cli()` перед настройкой периферии исключает гонки при старте. --- ## Алгоритмы +### Алгоритм сопряжения (pairing) + +Конечный автомат `zh_do_pairing()` работает в четырёх состояниях: + +1. **`ZH_RF_PAIR_SEND`** — генерируется псевдослучайный токен, формируется пакет запроса (`zh_rf_sensor_pairing_data_t` с `magic = ZH_RF_PAIRING_REQ_MAGIC`), выполняется неблокирующая передача через `zh_nrf24_send_no_wait()`. +2. **`ZH_RF_PAIR_TX_WAIT`** — ожидание прерывания IRQ (завершение передачи). Если пин IRQ уже низкий — событие считается наступившим. Иначе — переход в сон с watchdog-таймером 250 мс. При флаге `TX_DS` — переход в приём, при `MAX_RT` — в backoff. +3. **`ZH_RF_PAIR_RX_WAIT`** — ожидание ответа шлюза. При получении пакета проверяется: длина равна `ZH_RF_PAIRING_PACKET_SIZE`, `magic == ZH_RF_PAIRING_ACK_MAGIC`, тип совпадает, токен совпадает, `id != 0` и `id != DEVICE_ID_UNASSIGNED`. При успехе — сохранение `id` в EEPROM и возврат. +4. **`ZH_RF_PAIR_BACKOFF`** — переход в Power Down, ожидание 4 с через watchdog. После `PAIRING_TIMEOUT_ATTEMPTS` (7) неудачных попыток — переход в `zh_enter_final_sleep()`. + ### Алгоритм измерения напряжения батареи -- ADC конфигурируется с внутренним опорным напряжением 1,1 В на канале внутреннего источника 1,1 В (`REFS0=1`, `MUX[3:1]=111`), вход AVcc сравнивается с опорой. -- Предделитель частоты: 64 (ADPS[2:0] = 111), что даёт ~125 кГц при 8 МГц. +- АЦП конфигурируется с внутренним опорным напряжением 1,1 В на канале внутреннего источника 1,1 В (`REFS0=1`, `MUX[3:1]=111`), измеряется отношение AVcc к опоре 1,1 В. +- Предделитель частоты: 64 (`ADPS[2:0] = 111`), что даёт ~125 кГц при 8 МГц. - Выполняются **две последовательные конверсии**: первая стабилизирует выборку, вторая даёт результат. - Формула расчёта: `Vbat = (1024.0 × 1.1) / adc_value`. -- При нулевой выборке (`adc == 0`) возвращается 0.0. +- При нулевой выборке (`adc == 0`) возвращается `0.0`. - После измерения АЦП отключается сбросом бита `ADEN`. -### Алгоритм передачи через nRF24L01+ +### Алгоритм генерации псевдослучайного токена + +- Инициальное значение — регистр `MCUSR` (состояние сброса). +- АЦП запускается на 16 конверсий в канале с внутренним опорным напряжением. +- На каждой итерации: `token = (token << 1) ^ (token >> 15) ^ ADC` — сдвиг с переносом и XOR с шумом АЦП. +- Если результат равен 0, возвращается константа `0xACE1`. + +### Алгоритм блокирующей передачи через nRF24L01+ - Переход в Power Up (бит `PWR_UP` в CONFIG). - Ожидание 5 мс (время стартования осциллятора). - Очистка TX FIFO командой `FLUSH_TX`. - Загрузка полезной нагрузки командой `W_TX_PAYLOAD`. -- Импульс CE (установка в 1) для запуска передачи. -- Ожидание завершения по пину IRQ (PD2) с таймаутом 100 мс. -- Сброс флагов статуса, очистка FIFO, переход в Power Down. +- Установка CE в 1 для запуска передачи. +- Ожидание спада пина IRQ (PD2) с таймаутом 100 мс (проверка каждые 1 мс). +- Сброс CE, чтение статуса, сброс флагов, очистка FIFO, переход в Power Down. ### Алгоритм сна и пробуждения - Главный цикл проверяет флаг `send_request`. - Если флаг не установлен — вызывается `sleep_cpu()` (режим Power-down). - Пробуждение происходит по аппаратному прерыванию INT1 (rising edge на PD3). -- ISR устанавливает `send_request = 1`, цикл просыпается и выполняет передачу. +- ISR устанавливает `send_request = true`, цикл просыпается и выполняет передачу. --- @@ -132,40 +190,59 @@ ### Типовой цикл работы (main) ```c +#include "avr/io.h" +#include "avr/sleep.h" +#include "avr/interrupt.h" +#include "avr/wdt.h" +#include "math.h" +#include "stdbool.h" +#include "zh_rf_config.h" + +static volatile uint8_t send_request = false; +zh_rf_sensor_transmitted_data_t transmitted_data = {0}; + +ISR(INT1_vect) +{ + send_request = true; +} + int main(void) { - // Отключаем прерывания на время инициализации + transmitted_data.type = ZH_RF_SENSOR_TOUCH_SWITCH; + MCUSR &= ~(1 << WDRF); + wdt_disable(); cli(); - // Настраиваем периферию zh_pad_interrupt_init(); zh_spi_init(); zh_nrf24_init(); - // Конфигурируем режим сна Power-down set_sleep_mode(SLEEP_MODE_PWR_DOWN); sei(); - - // Глобальная структура из zh_rf_config - zh_rf_sensor_transmitted_data_t transmitted_data = - {DEVICE_ID, ZH_RF_SENSOR_TOUCH_SWITCH, 0, 0, 0, 0, 0}; - + // Загрузка или получение идентификатора устройства + 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; for (;;) { - if (send_request == 1) + if (send_request == true) { - // Собираем и отправляем пакет cli(); - send_request = 0; + send_request = false; sei(); - transmitted_data.battery_voltage = - (int16_t)(zh_get_battery_voltage() * 100.0); - zh_nrf24_send((uint8_t *)&transmitted_data, - sizeof(transmitted_data)); + transmitted_data.battery = (int16_t)lroundf(zh_get_battery_voltage() * 100.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 { - // Спим до следующего прерывания cli(); - if (send_request == 0) + if (send_request == false) { sleep_enable(); sei(); @@ -175,60 +252,56 @@ int main(void) sleep_disable(); } } + return 0; } ``` -### Изменение конфигурации под другое устройство +### Назначение пинов AVR для nRF24L01+ ```c -// Измените идентификатор для нового устройства -#define DEVICE_ID 7 // Уникальный ID в сети -// Тип устройства задаётся перечислением ZH_RF_SENSOR_TOUCH_SWITCH +// PB2 (SS) -> CSN (выбор кристалла SPI, активный низкий) +// PB3 (MOSI) -> MOSI +// PB4 (MISO) -> MISO +// PB5 (SCK) -> SCK +// PB1 -> CE (запуск передачи/приёма) +// PD2 (INT0) -> IRQ (вход, активный низкий, с подтяжкой к VCC) +// PD3 (INT1) -> Выход сенсорного датчика (rising edge, без подтяжки) +``` + +### Изменение конфигурации шлюза + +```c +// Измените адрес и канал под ваш шлюз #define GATEWAY_CHANNEL 120 #define GATEWAY_ADDRESS 0xDDEEFF +// Тип устройства задаётся перечислением ZH_RF_SENSOR_TOUCH_SWITCH из zh_rf_config ``` -### Подключение по SPI (схема выводов) - -```c -// Назначение пинов AVR для nRF24L01+: -// PB2 (SS) -> CSN (управление SPI-селектом) -// PB3 (MOSI) -> MOSI -// PB5 (SCK) -> SCK -// PB1 -> CE (управление передачей) -// PD2 -> IRQ (вход, активный низкий, с подтяжкой к VCC) -// PD3 -> INT1 (вход сенсорного датчика, rising edge) -``` - ---- - -## Логирование - -Компонент не использует ESP-IDF логирование (прошивка для AVR, а не ESP32). Логирование отсутствует. - --- ## Ошибки -Компонент не возвращает коды ошибок. Все функции имеют тип `void` или возвращают однобайтовый статус SPI (`uint8_t`) для низкоуровневых операций. +Прошивка не возвращает коды ошибок в стиле ESP-IDF. Все функции имеют тип `void` или возвращают однобайтовый статус SPI (`uint8_t`) для низкоуровневых операций. | Условие | Поведение | -| --------- | ---------- | -| Нулевое показание АЦП | `zh_get_battery_voltage()` возвращает 0.0 | -| Превышение таймаута передачи | Передача прерывается, пакет теряется | -| Неверный аргумент | Не применимо — все функции `static`, аргументы не валидируются | +| --------- | ----------- | +| Нулевое показание АЦП | `zh_get_battery_voltage()` возвращает `0.0` | +| Превышение таймаута передачи (100 мс) | Передача прерывается, пакет теряется | +| Неверная длина принятого пакета при сопряжении | Пакет отбрасывается, переход в `ZH_RF_PAIR_BACKOFF` | +| Превышение числа попыток сопряжения (7) | `zh_enter_final_sleep()` — сон до сброса | +| `zh_rf_sensor_build_payload()` возвращает 0 | Передача пропускается, переход в `zh_enter_final_sleep()` | --- ## Ограничения -1. **Только передача (TX-only)** — компонент не принимает данные, шлюз не может отправить подтверждение на уровне приложения. +1. **Только передача (TX-only) в рабочем цикле** — приём используется исключительно на этапе сопряжения, шлюз не может отправить данные устройству в штатном режиме. 2. **Отсутствие шифрования** — CRC включён, но AES-шифрование не используется. Пакеты могут быть перехвачены. -3. **Жёстко зашитый адрес шлюза** — смена адреса требует перепрошивки. -4. **Таймаут передачи 100 мс** — если шлюз недоступен, пакет теряется без повторной попытки на уровне приложения (аппаратные ретрансмиты настроены через `SETUP_RETR = 0xFF`: 15 попыток с максимальным интервалом). -5. **Одно сенсорное событие за цикл** — несколько касаний во время передачи будут потеряны (флаг — один бит). +3. **Жёстко зашитый адрес и канал шлюза** — смена `GATEWAY_ADDRESS` или `GATEWAY_CHANNEL` требует перепрошивки. +4. **Таймаут блокирующей передачи 100 мс** — если шлюз недоступен, пакет теряется без повторной попытки на уровне приложения (аппаратные ретрансмиты настроены через `SETUP_RETR = 0xFF`: 15 попыток с максимальным интервалом). +5. **Одно сенсорное событие за цикл** — несколько касаний во время передачи будут потеряны (флаг `send_request` — один бит). 6. **Точность измерения батареи** — зависит от стабильности внутреннего опорного напряжения 1,1 В и может отличаться от реального напряжения на ±3%. -7. **Нет механизма глубокого сна для периферии** — АЦП полностью отключается после измерения (`ADEN = 0`), но SPI остаётся включённым в режиме сна. +7. **SPI остаётся включённым в режиме сна** — перед окончательным сном SPI отключается в `zh_enter_final_sleep()`, но в штатном режиме ожидания SPI не отключается между передачами. --- @@ -247,6 +320,8 @@ int main(void) ### Сборка и прошивка через PlatformIO ```bash +git clone https://github.com/aZholtikov/zh_avr_touch_pad +cd zh_avr_touch_pad pio run -e ATmega328P -t upload ``` @@ -260,12 +335,18 @@ 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 "avr/eeprom.h" +#include "avr/wdt.h" #include "util/delay.h" +#include "math.h" +#include "stdbool.h" #include "zh_rf_config.h" ```