feat: added sensor pairing to gateway

This commit is contained in:
aZholtikov committed 2026-10-24 10:03:54 +03:00
1 parent cd81e27665
commit 4514ce4a10
4 files changed
+638 -112

No files matched your search

+4 -3
View File
@@ -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
+6 -6
View File
@@ -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
https://github.com/aZholtikov/zh_rf_config#dev
+463 -19
View File
@@ -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;
+165 -84
View File
@@ -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"
```